Integrating with SwiftUI -- Records Do Not Save?

Going by 5.19.0 GRDBCombineDemo, I'm attempting to simply use the saved() or inserted() api(whichever I can get working first but would prefer the saved() api).

I placed the AppDatabase.swift, Persisted.swift and Player.swift files in my code from the demo and I'm also plan on using the GRDBQuery sugar(once I get saving working).

I also made the database available with the environment object.

So my workflow is Airdrop a CSV document and parse it in a view model which has been completed.

Now I want to store values from the CSV but for the life of me I can't get GRDB to save anything(I confirm this by obtaining .xcappdata from the device and inspecting the db.sqlite)

I am absolutely sure my code should work but GRDB is silently failing. Just to get things working I am saving the demo app Player records to the db using the first names found in the CSV file. When I open the file I only see the eight players randomly-generated with createRandomPlayersIfEmpty()

I made a slight change to AppDatabase.swift where I catch any errors and print them(but no errors result in the log):

    /// Saves (inserts or updates) a player. When the method returns, the
    /// player is present in the database, and its id is not nil.
    func savePlayer(_ player: inout Player) throws {
        if player.name.isEmpty {
            throw ValidationError.missingName
        }
        do {
            try dbWriter.write { db in
                try player.save(db)
            }
        } catch {
            print("savePlayer save error: \(error)")
        }

    }

I am using Xcode 13.2.1 on Monterey 12.1. iPad running iPadOS 15.2.1

Any ideas? Here are the files I'm using. Look for the line
savePlayer(name: rawStudent[1], score: Player.randomScore())

in HomeViewModel.swift

Hello @mazz,

Silent failure is a hint that you do not save in the database you expect (assuming you don't catch errors and ignore them).

You took inspiration from the GRDBCombineDemo demo app. Look how it does not always use the same database: there's a database on disk for the regular app, and also temporary in-memory databases for SwiftUI previews and tests (Persistence.swift).

The database that is used is the one that is put into your \.appDatabase environment key. In the demo app, it is, by default, an empty in-memory database. Just make sure that it is set to the one you want, and I think your issue will be solved.

Thank you for taking the time to explain the issue! Of course that is root cause.

For others who fell into the same trap, I just refer to the shared db:

    func savePlayer(name: String, score: Int) {
        do {
            var player = Player(id: nil, name: name, score: score)
            print("save player: \(player)")

            try AppDatabase.shared.savePlayer(&player)
        } catch {
            print("savePlayer error: \(error)")
        }
    }

I'm happy I guessed right :-)

For others who fell into the same trap, I just refer to the shared db:

It's good that you solved your issue. Yet my advice is to use the environment database instead, the one that is available through the @Environment(\.appDatabase) private var appDatabase property. Why define this property if it is not used?

I get this error if I do:
try appDatabase.shared.savePlayer(&player)

Static member 'shared' cannot be used on instance of type 'AppDatabase'

Like you mentioned, the defaultValue refers to the .empty() and not the .shared

Make another key to the shared db?

@main
struct MarkablyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            ContentView().environment(\.appDatabase, .shared)
        }
    }
}

... 

private struct AppDatabaseKey: EnvironmentKey {
    static var defaultValue: AppDatabase { .empty() }
}

extension EnvironmentValues {
    var appDatabase: AppDatabase {
        get { self[AppDatabaseKey.self] }
        set { self[AppDatabaseKey.self] = newValue }
    }
}

Hmmm... Maybe SwiftUI does not provide the expected environment value to your ViewModel. If this is the case, then maybe the @Environment(\.appDatabase) private var appDatabase property should be removed entirely from the ViewModel which can't access it. The topic of telling SwiftUI to give environment values to view models is not stricto sensu about GRDB (it's a SwiftUI topic), so I'd rather not give any advice here.

Fair enough. So here is a possible solution: add another key(naming could probably be improved):

private struct AppDatabaseKey: EnvironmentKey {
    static var defaultValue: AppDatabase { .empty() }
}

private struct AppDatabaseSharedKey: EnvironmentKey {
    static var defaultValue: AppDatabase { .shared }
}

extension EnvironmentValues {
    var appDatabase: AppDatabase {
        get { self[AppDatabaseKey.self] }
        set { self[AppDatabaseKey.self] = newValue }
    }

    var appDbShared: AppDatabase {
        get { self[AppDatabaseSharedKey.self] }
        set { self[AppDatabaseSharedKey.self] = newValue }
    }
}

Though this would have a side-effect on the @Query syntactic sugar:

extension Query where Request.DatabaseContext == AppDatabase {
    /// Convenience initializer for requests that feed from `AppDatabase`.
    init(_ request: Request) {
        self.init(request, in: \.appDatabase)
    }
}

It would seem @Query would have to choose between either the .shared db or the .empty() db?