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") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Turns out that using
sharing-grdb
andswift-dependencies
is discouraged by Pointfree – so I got rid of that abstraction.