Last active
June 16, 2025 08:45
-
-
Save appfrosch/276fe60f3f73a553cc6f775b48ed2a65 to your computer and use it in GitHub Desktop.
Use `TCA`, `GRDB` and the `swift-dependencies` together
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Sharing_GRDB_in_TCAApp.swift | |
// Sharing-GRDB-in-TCA | |
// | |
// Created by Andreas Seeger on 15.06.2025. | |
// | |
import ComposableArchitecture | |
import OSLog | |
import SharingGRDB | |
import SwiftUI | |
@main | |
struct Sharing_GRDB_in_TCAApp: App { | |
@State var store: StoreOf<ItemListFeature> | |
init() { | |
prepareDependencies { | |
$0.defaultDatabase = try! appDatabase() | |
} | |
self.store = Store( | |
initialState: ItemListFeature.State()) { | |
ItemListFeature() | |
} | |
} | |
var body: some Scene { | |
WindowGroup { | |
ItemListView(store: store) | |
} | |
} | |
} | |
// MARK: Item Feature | |
@Reducer | |
struct ItemListFeature { | |
@Dependency(\.defaultDatabase) var defaultDatabase | |
@ObservableState | |
struct State { | |
@FetchAll var items: [Item] | |
} | |
enum Action { | |
case addItem | |
} | |
var body: some Reducer<State, Action> { | |
Reduce { state, action in | |
switch action { | |
case .addItem: | |
return .run { _ in | |
do { | |
let newItem = Item(name: "New Item") | |
try defaultDatabase.write { db in | |
try Item.insert { newItem } | |
.execute(db) | |
} | |
} catch { | |
logger.error("Failed to add item: \(error)") | |
} | |
} | |
} | |
} | |
} | |
} | |
//MARK: Item View | |
struct ItemListView: View { | |
@Bindable var store: StoreOf<ItemListFeature> | |
var body: some View { | |
NavigationView { | |
List { | |
if store.items.isEmpty { | |
ContentUnavailableView { | |
Text("No items available. Tap the plus button to add a new item.") | |
} | |
} | |
ForEach(store.items) { item in | |
Text(item.name) | |
} | |
} | |
.toolbar { | |
ToolbarItem(placement: .navigationBarTrailing) { | |
Button("Add Item", systemImage: "plus") { | |
store.send(.addItem) | |
} | |
} | |
} | |
} | |
} | |
} | |
#Preview { | |
let store = Store( | |
initialState: ItemListFeature.State()) { | |
ItemListFeature() | |
} | |
ItemListView(store: store) | |
} | |
//MARK: Item Model | |
@Table | |
struct Item: Identifiable { | |
let id: UUID | |
let name: String | |
init( | |
id: UUID = UUID(), | |
name: String | |
) { | |
self.id = id | |
self.name = name | |
} | |
} | |
//MARK: App Database | |
func appDatabase() throws -> any DatabaseWriter { | |
@Dependency(\.context) var context | |
var configuration = Configuration() | |
configuration.foreignKeysEnabled = true | |
#if DEBUG | |
configuration.prepareDatabase { db in | |
db.trace(options: [.profile, .statement]) { event in | |
switch event { | |
case .statement(let sql): | |
logger.debug("SQL: \(sql)") | |
case .profile(let sql, let duration): | |
logger.debug("SQL: \(sql) (\(duration)s)") | |
} | |
} | |
} | |
#endif | |
let path = URL.documentsDirectory.appendingPathComponent("db.sqlite").path() | |
logger.info("open \(path)") | |
#if DEBUG | |
// Delete the database file if it exists if needed | |
// try? FileManager.default.removeItem(atPath: path) | |
#endif | |
let database = try DatabasePool(path: path, configuration: configuration) | |
var migrator = DatabaseMigrator() | |
#if DEBUG | |
migrator.eraseDatabaseOnSchemaChange = true | |
#endif | |
migrator.registerMigration("Create items table") { db in | |
try db.create(table: "items") { t in | |
t.primaryKey("id", .text) | |
t.column("name", .text).notNull() | |
} | |
} | |
try migrator.migrate(database) | |
return database | |
} | |
private let logger = Logger(subsystem: "ch.appfros.Sharing-Grdb-in-TCA", category: "Database") |
Turns out that using sharing-grdb
and swift-dependencies
is discouraged by Pointfree – so I got rid of that abstraction.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Requires the packages
swift-composable-architecture
andsharing-grdb
(though I am not quite sure to what extend I am even usingsharing-grdb
at this stage–the customItemRepository
is heavily leveragingGRDB
andDependency
and not using@FetchAll
at the moment …)Cool separation of concern anyways in my opinion (also, was only able to spin this up with the help of Cursor 🥴)