GRDB <3 Combine + SwiftUI

A very early GRDBCombine package is available on https://github.com/groue/GRDBCombine. It comes with a very simple demo app which uses DatabasePublisher, and the DatabasePublished property wrapper, in order to keep UI in sync with the database content.

This is pretty experimental code. Known issues:

  • Only use it from the main thread
  • The API for defining the observed database values needs some love.

Many thanks to @Tony_Parker and folks in the Foundation team!

7 Likes

Wow, good job Apple :clap:

The ViewModel fueled by DatabasePublished can trivially be turned into an EnvironmentObject that feeds a SwiftUI View, with free tableView animations :heart:

The demo app has been upgraded in the GRDBCombine repo.

In the end, what do we have? A pile of layers that click very well together:

  • GRDB
    • Record Types that can read and write in the database.
    • The Query Interface that can generate SQL when you don't want to.
    • ValueObservation that can observe the results of one or several database requests. Observed values are guaranteed to be fetched from a consistent database state, that's what's super cool, and super important, with Value Observation.
  • GRDBCombine
    • DatabasePublishers.Value, a publisher that you build from a ValueObservation.
    • DatabasePublished, a property wrapper that embeds a DatabasePublisher and keeps a property up-to-date with database content, ready to feed a SwiftUI view.
4 Likes

Very nice! The outer struct / inner class is the pattern we use for nearly all the operators in Combine as well. Thanks for sharing.

1 Like

Thanks Tony. Your validation of the initial path is much welcomed :-)

GRDBCombine is almost ready, with some inline doc, improved robustness and scheduling control, and an upgraded demo app which embeds all the good practices I could think of.

On a side note, I wish to thank our friends from Point-Free (@stephencelis, maybe?) . Their article How to Control the World provides a nice way to inject dependencies at a static level, which the DatabasePublished property wrapper requires:

class HallOfFameViewModel: BindableObject {
    // Here the database needs to be injected somehow.
    @DatabasePublished(Players.hallOfFame(maxPlayerCount: 10))
    private var hallOfFame: Result<Players.HallOfFame, Error>
    ...
}

// Here is how:
enum Players {
    static func hallOfFame(maxPlayerCount: Int)
        -> DatabasePublishers.Value<HallOfFame> 
    {
        // --------------------------------------v 👍
        return DatabasePublishers.Value(..., in: Current.database())
    }
}

This pattern allows the hallOfFame publisher to be tested outside of the application, with an in-memory SQLite database for example.