Skip to content

Instantly share code, notes, and snippets.

@appfrosch
Last active June 16, 2025 08:45
Show Gist options
  • Save appfrosch/276fe60f3f73a553cc6f775b48ed2a65 to your computer and use it in GitHub Desktop.
Save appfrosch/276fe60f3f73a553cc6f775b48ed2a65 to your computer and use it in GitHub Desktop.
Use `TCA`, `GRDB` and the `swift-dependencies` together
//
// 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")
@appfrosch
Copy link
Author

appfrosch commented Jun 15, 2025

Requires the packages swift-composable-architecture and sharing-grdb (though I am not quite sure to what extend I am even using sharing-grdb at this stage–the custom ItemRepository is heavily leveraging GRDB and Dependency 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 🥴)

@appfrosch
Copy link
Author

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