A property wrapper for displaying up-to-date database content in SwiftUI views

Overview

@Query

Latest release: November 25, 2021 • version 0.1.0CHANGELOG

Requirements: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ • Swift 5.5+ / Xcode 13+


This package provides the @Query property wrapper, that lets your SwiftUI views automatically update their content when the database changes.

import GRDBQuery
import SwiftUI

/// A view that displays an always up-to-date list of players in the database.
struct PlayerList: View {
    @Query(AllPlayers())
    var players: [Player]
    
    var body: some View {
        List(players) { player in
            Text(player.name)
        }
    }
}

@Query is for GRDB what @FetchRequest is for Core Data. Although @Query does not depend on GRDB, it was designed with GRDB in mind.

Why @Query?

@Query solves a tricky SwiftUI challenge. It makes sure SwiftUI views are immediately rendered with the database content you expect.

For example, when you display a List that animates its changes, you do not want to see an animation for the initial state of the list, or to prevent this undesired animation with extra code.

You also want your SwiftUI previews to display the expected values without having to run them.

Techniques based on onAppear(perform:), onReceive(_:perform) and similar methods suffer from this "double-rendering" problem and its side effects. By contrast, @Query has you fully covered.

Usage

To use @Query, first define a new environment key that grants access to the database.

In the example below, we define a new dbQueue environment key whose value is a GRDB DatabaseQueue. Some other apps, like the GRDB demo apps, can choose another name and another type, such as a "database manager" that encapsulates database accesses.

The EnvironmentKey documentation describes the procedure:

import GRDB
import SwiftUI

private struct DatabaseQueueKey: EnvironmentKey {
    /// The default dbQueue is an empty in-memory database
    static var defaultValue: DatabaseQueue { DatabaseQueue() }
}

extension EnvironmentValues {
    var dbQueue: DatabaseQueue {
        get { self[DatabaseQueueKey.self] }
        set { self[DatabaseQueueKey.self] = newValue }
    }
}

You will substitute the default empty database with an actual database on disk for your main application:

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            MyView().environment(\.dbQueue, /* some DatabaseQueue on disk */)
        }
    }
}

You will feed SwiftUI previews with databases that you want to preview:

struct PlayerList_Previews: PreviewProvider {
    static var previews: some View {
        // Empty list
        PlayerList().environment(\.dbQueue, /* empty table of players */)
        
        // Non-empty list
        PlayerList().environment(\.dbQueue, /* non-empty table of players */)
    }
}

See the GRDB demo apps for examples of such setups.

Next, define a Queryable type for each database request you want to observe.

For example:

import Combine
import GRDB
import GRDBQuery

/// Tracks the full list of players
struct AllPlayers: Queryable {
    static var defaultValue: [Player] { [] }
    
    func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<[Player], Error> {
        ValueObservation
            .tracking(Player.fetchAll)
            // The `.immediate` scheduling feeds the view right on subscription,
            // and avoids an initial rendering with an empty list:
            .publisher(in: dbQueue, scheduling: .immediate)
            .eraseToAnyPublisher()
    }
}

The Queryable protocol has two requirements: a default value, and a Combine publisher. The publisher is built from the DatabaseQueue stored in the environment (you'll adapt this sample code if you prefer another type). The publisher tracks database changes with GRDB ValueObservation. The default value is used until the publisher publishes its initial value.

In the above sample code, we make sure the views are immediately fed with database content with the scheduling: .immediate option. This prevents any "blank state", or "flash of missing content".

The scheduling: .immediate option should be removed for database requests that are too slow. In this case, views are initially fed with the default value, and the database content is notified later, when it becomes available. In the meantime, your view can display some waiting indicator, or a redacted placeholder.

Finally, you can define a SwiftUI view that automatically updates its content when the database changes:

import GRDBQuery
import SwiftUI

struct PlayerList: View {
    @Query(AllPlayers(), in: \.dbQueue)
    var players: [Player]
    
    var body: some View {
        List(players) { player in
            HStack {
                Text(player.name)
                Spacer()
                Text("\(player.score) points")
            }
        }
    }
}

@Query exposes a binding to the request, so that views can change the request when they need. The GRDB demo apps, for example, use a Queryable type that can change the player ordering:

struct PlayerList: View {
    // Ordering can change through the $players.ordering binding.
    @Query(AllPlayers(ordering: .byScore))
    var players: [Player]
    ...
}

As a convenience, you can also define a dedicated Query initializer to use the dbQueue environment key automatically:

extension Query where Request.DatabaseContext == DatabaseQueue {
    init(_ request: Request) {
        self.init(request, in: \.dbQueue)
    }
}

This improves clarity at the call site:

struct PlayerList: View {
    @Query(AllPlayers())
    var players: [Player]
    ...
}

How to Handle Database Errors?

By default, @Query ignores errors published by Queryable types. The SwiftUI views are just not updated whenever an error occurs. If the database is unavailable when the view appears, @Query will just output the default value.

You can restore error handling by publishing a Result, as in the example below:

import Combine
import GRDB
import GRDBQuery

struct AllPlayers: Queryable {
    static var defaultValue: Result<[Player], Error> { .success([]) }
    
    func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<Result<[Player], Error>, Never> {
        ValueObservation
            .tracking(Player.fetchAll)
            .publisher(in: dbQueue, scheduling: .immediate)
            .map { players in .success(players) }
            .catch { error in Just(.failure(error)) }
            .eraseToAnyPublisher()
    }
}

Demo Application

This package ships with a demo app. See also the GRDB demo apps for various examples of apps that use @Query.


🙌 @Query was vastly inspired from Core Data and SwiftUI by @davedelong, with a critical improvement contributed by @steipete. Many thanks to both of you!

Comments
  • `mirrorAppearanceState` and initially hidden views

    `mirrorAppearanceState` and initially hidden views

    I have a View within a TabView, and I'm using .mirrorAppearanceState on the View. Initially the View's tab is not selected and thus the View is not visible.

    The problem is that the View will now keep auto updating in the background until the respective tab is selected for the first time. onDisappear is not called because the View never appeared in the first place.

    opened by kabouzeid 10
  • [Bug?] SwiftUI warning in new betas

    [Bug?] SwiftUI warning in new betas

    Hey,

    the new Xcode and iOS 16 beta now show a warning which reads as following:

    Publishing changes from within view updates is not allowed, this will cause undefined behavior.

    The highlighted line in code is this one:

    https://github.com/groue/GRDBQuery/blob/22df8987f1d0c670970fd730b58b445ae01ce312/Sources/GRDBQuery/Query.swift#L406

    I'm not sure if this is just a bug in the new beta, because I haven't had that warning before, or if they changed something in SwiftUI.

    I've seen a similar issue when googling the warning on mongodb's realm GitHub: https://github.com/realm/realm-swift/issues/7908

    and some posting on the apple developer forum so it is definitely not a problem which only affects GRDBQuery.

    opened by fbertuzzi 7
  • No animation on change

    No animation on change

    Hello thanks for this tiny lib around combine support in GRDB (and clever I might add)

    I am currently trying to migrate a personal app from CoreData to GRDB, and I am finally enjoying querying data in my app so thanks for this 😄

    But I am stumbling across a problem I don't quite know how to solve...

    How can we animate change to @Query property with some kind of withAnimation {} to be automatically reflected inside the UI ? I naively tried to surround model.save(db) with withAnimation {} but as you might have guessed it doesn't work since updated value are coming from ValueObservation, I also tried around ValueObservation but it's the same

    Happy to move this to a discussion if that's not the correct place and just a mistake on my side

    question 
    opened by AzSiAz 7
  • How to install?

    How to install?

    (Sorry for the noob question)

    I'm using Cocoapods in my app. Adding pod 'GRDBQuery' to the Podfile doesn't work. Should I import this project as a git submodule? What's the recommended way to get GRDBQuery added to an existing project/app?

    opened by disbelief 6
  • @Query support DatabasePool as DatabaseContext?

    @Query support DatabasePool as DatabaseContext?

    GRDB docs indicate that DatabasePool and DatabaseQueue can be used the same way, but this does not extend to GRDBQuery.

    It seems if Query could work with a pool the same way GRDB does in its api it would work as is Maybe I'm missing something as the docs say to only ever open one connection to 1 file be it pool or queue, which if so really makes it seem like Query needs to work with pools too. I understood it as a pool allowed multiple access to the same file from different locations.

    opened by jhoughjr 5
  • Is there a way to reload/refresh?

    Is there a way to reload/refresh?

    Hey,

    I'm currently trying to reload a @Query as my database is changed on another Process (NotificationService Extension to be exact). I'm using a DarwinNotificationCenter as described here to notify my view.

    The problem that I have now, is that I can't reload the @Query at least I don't know how to do it. As it's not a State variable I cannot modify it. Is there maybe a builtin way in @Query or GRDB itself, which I don't know of to trigger a refresh?

    opened by fbertuzzi 5
  • Possibility to use this without the environment

    Possibility to use this without the environment

    I really like the idea of this propertyWrapper! I was wondering though if it would be possible to use the wrapper without having an environment, but reusing a DatabaseQueue directly from a parameter?

    My views already hold an object in which the database context lives, so I was wondering if I can initialize a property annotated with @Query in the init block. This saves me some lines and it won't create a default in memory database.

    Like this (just some pseudo code):

    @Query
    var players: [Player]
    
    private let objectWithDatabase: MyObject
    
    init(objectWithDatabase: MyObject) { 
        self.objectWithDatabase = objectWithDatabase
        self.players = .init(database: objectWithDatabase.db, ...)
    }
    

    Thanks!

    opened by Jasperav 5
  • Strategies for Queryable structs that need a dynamic value in SwiftUI

    Strategies for Queryable structs that need a dynamic value in SwiftUI

    In the demo application dealing with the Player model, consider if you had a model called Team, which has many Player identifiers associated to it.

    In such a case, you might have a view where you can toggle the Team you're viewing. To get at the data, I'd construct such a Query type like this to drive it:

    struct TeamQueryableRequest: Queryable {
        var teamId: Int64
        static var defaultValue: [Team] { [] }
        
        func publisher(in appDatabase: AppDatabase) -> AnyPublisher<[Team], Error> {
            ValueObservation
                .tracking(fetchValue(_:))
                .publisher(
                    in: appDatabase.databaseReader,
                    scheduling: .immediate)
                .eraseToAnyPublisher()
        }
        
        func fetchValue(_ db: Database) throws -> [Release] {
            return try Team(id: teamId).fetchAll(db)
        }
    }
    

    If you just present the View to see a Team in a one-off manner such as a modal sheet, such as in the demo application, then it is acceptable to pass the id in the initializer:

    struct TeamView: View {    
        @Query< TeamQueryableRequest >
        private var teams: [Team]
            
        init(id: Int64) {
            _ teams = Query(TeamQueryableRequest(teamId: id))
        }
    }
    

    The issue I am having is this: That view is always displayed (this is a Mac app I'm dealing with, where the view hierarchy is much more flattened than iOS) and the id changes dynamically as the user toggles which Team they are viewing from an EnvironmentObject that is passed around, i.e.:

    class Store: ObservableObject {
        @Published var selectedTeamId: Int64 = 0
    }
    

    Due to Swift's strict initializer rules, I'm unable to do something like this:

    struct TeamView: View {    
        @EnvironmentObject var store: Store
        @Query< TeamQueryableRequest >
        private var teams: [Team]
           
        init(id: Int64) {
            _ teams = Query(TeamQueryableRequest(teamId: store.selectedTeamId)) ~~~Compiler Error~~~
        }
    }
    

    TL;DR: How do I leverage Query when it relies on data that can't be used until after initialization?

    Is there a good way to handle this, it feels like I'm making it too difficult. Thank you for any advice, and happy to expand if I need to.

    opened by DreamingInBinary 4
  • Question: How would you use GRDBQuery on large data

    Question: How would you use GRDBQuery on large data

    Hey,

    I'm currently working on a Chat App and using GRDBQuery for data loading from GRDB.

    How would you use GRDBQuery for loading chat message? Currently I'm fetching all messages which starts to cause large loading times and memory usage as it's all getting fetched at once.

    The messages are displayed in a ScrollView which embeds a LazyVStack.

    How would I use GRDBQuery for fetching just the messages currently needed, but keep the immediate loading GRDBQuery offers.

    Is that possible?

    opened by fbertuzzi 4
  • @Query property not being restored by SwiftUI when view is updated

    @Query property not being restored by SwiftUI when view is updated

    Given a view with a @Query wrapped property:

    struct PlayerView: View {
        @Query<PlayerRequest>
        private var player: Player?
    
        init(id: Int) {
            self._ player = Query(PlayerRequest(id: id))
        }
    }
    

    Let’s put it into our app:

    struct ContentView: View {
        @State private var id: Int = 0
        var body: some View {
            PlayerView(id: id)
            Button("Change Player") {
                self.id += 1
            }
        }
    }
    

    When id gets updated, just like @State, SwiftUI restores the previously cached @Query and overwrites the value we set in the initializer (basically described here).

    This means the PlayerView never gets updated. I can introduce an explicit identity to force the update:

    PlayerView(id: id)
    .id(id)
    

    Is there a better approach? Thanks.

    opened by baoshan 3
  • SwiftUI views using EnvironmentStateObject receive multiple init calls

    SwiftUI views using EnvironmentStateObject receive multiple init calls

    (Disclaimer: I'm fairly new to SwiftUI and GRDBQuery)

    I've noticed that my SwiftUI views that use @EnvironmentStateObject to inject their view models are having init called multiple times. I'm not sure if this is intended behaviour or not, but it places some restrictions on what can be done in the makeObject block if it gets called multiple times.

    Here's an example:

    struct MyView: View {
        @EnvironmentStateObject private var viewModel: MyViewModel
        
        init() {
            print("MyView.init")
            
            _viewModel = EnvironmentStateObject {
                MyViewModel(dependencies: $0.dependencies)
            }
    
        var body: some View {
            Button {
                viewModel.doStuff()
            } label: {
                Text(viewModel.status)
            }
        }
    }
    
    final class MyViewModel: ObservableObject {
        @Published var status: String = "Idle"
    
        private var stuffUseCase: StuffUseCase
    
        init(dependencies: MyDependencies) {
            print("MyViewModel.init")
            self.stuffUseCase = dependencies.getStuffUseCase()
        }
    
        func doStuff() {
            self.status = "Working"
            DispatchQueue.global().async {
                self.stuffUseCase.doStuff()
                self.status = "Done"
            }
        }
    }
    

    When my app starts, the logs show the following before any navigation or interaction:

    MyView.init
    MyViewModel.init
    MyView.init
    MyViewModel.init
    

    Is this to be expected?

    opened by disbelief 3
Owner
Gwendal Roué
iOS app developer, and author of a few open source libraries
Gwendal Roué
Swift library that makes easier to serialize the user's preferences (app's settings) with system User Defaults or Property List file on disk.

PersistentStorageSerializable PersistentStorageSerializable is a protocol for automatic serialization and deserialization of Swift class, struct or NS

Ivan Rublev 163 Jun 3, 2021
Shows the issue with swift using an ObjC class which has a property from a swift package.

SwiftObjCSwiftTest Shows the issue with swift using an ObjC class which has a property from a swift package. The Swift class (created as @objc derived

Scott Little 0 Nov 8, 2021
Listens to changes in a PostgreSQL Database and via websockets.

realtime-swift Listens to changes in a PostgreSQL Database and via websockets. A Swift client for Supabase Realtime server. Usage Creating a Socket co

Supabase 35 Dec 1, 2022
Realm is a mobile database: a replacement for Core Data & SQLite

Realm is a mobile database that runs directly inside phones, tablets or wearables. This repository holds the source code for the iOS, macOS, tvOS & wa

Realm 15.7k Jan 1, 2023
WCDB is a cross-platform database framework developed by WeChat.

WCDB 中文版本请参看这里 WCDB is an efficient, complete, easy-to-use mobile database framework used in the WeChat application. It's currently available on iOS,

Tencent 9.6k Jan 8, 2023
Sync Realm Database with CloudKit

IceCream helps you sync Realm Database with CloudKit. It works like magic! Features Realm Database Off-line First Thread Safety Reactive Programming O

Soledad 1.8k Jan 6, 2023
Movies Information DataBase (Add - Delete - Edit - Search)

MoviesInformation Movies Information DataBase (Add - Delete - Edit - Search) This Code Provide Simple Program About Movies Information This Program Ca

Mohammad Jaha 2 Sep 15, 2021
Easy direct access to your database 🎯

OHMySQL ★★ Every star is appreciated! ★★ The library supports Objective-C and Swift, iOS and macOS. You can connect to your remote MySQL database usin

Oleg 210 Dec 28, 2022
Safe and easy wrappers for common Firebase Realtime Database functions.

FirebaseHelper FirebaseHelper is a small wrapper over Firebase's realtime database, providing streamlined methods for get, set, delete, and increment

Quan Vo 15 Apr 9, 2022
A Generic CoreData Manager to accept any type of objects. Fastest way for adding a Database to your project.

QuickDB FileManager + CoreData ❗️ Save and Retrieve any thing in JUST ONE line of code ❗️ Fast usage dataBase to avoid struggling with dataBase comple

Behrad Kazemi 17 Sep 24, 2022
A simple order manager, created in order to try Realm database

Overview A simple order manager, created in order to get acquainted with the features and limitations of the local Realm database. The project is writ

Kirill Sidorov 0 Oct 14, 2021
A sample application showcasing Vapor 4 connecting to an Oracle database using SwiftOracle package.

vapor-oracle A sample application showcasing Vapor 4 connecting to an Oracle database using SwiftOracle package. In this Vapor application, we create

Ilia Sazonov 3 Sep 22, 2022
A food delivery app using firebase as the database.

FDA-ONE Food Delivery Application is a mobile application that users can use to find the best restaurant around their location and order the meals the

Naseem Oyebola 0 Nov 28, 2021
BucketServer - Small API with SQLite database that saves notes for an iOS appliction called Bucket list

BucketList Server-Side Small API with SQLite database that saves notes for an iO

null 0 Dec 30, 2021
Innova CatchKennyGame - The Image Tap Fun Game with keep your scores using Core Database

Innova_CatchKennyGame The Image Tap Fun Game with keep your scores using Core Da

Alican Kurt 0 Dec 31, 2021
Synco - Synco uses Firebase's Realtime Database to synchronize data across multiple devices, in real time

Synco Synco uses Firebase's Realtime Database to synchronize a color across mult

Alessio 0 Feb 7, 2022
PostgreSQL database adapter (ORM included)

PostgreSQL PostgreSQL adapter for Swift 3.0. Conforms to SQL, which provides a common interface and ORM. Documentation can be found there. Installatio

Zewo Graveyard 91 Sep 9, 2022
CodableCloudKit allows you to easily save and retrieve Codable objects to iCloud Database (CloudKit)

CodableCloudKit CodableCloudKit allows you to easily save and retrieve Codable objects to iCloud Database (CloudKit) Features ℹ️ Add CodableCloudKit f

Laurent Grondin 65 Oct 23, 2022
A type-safe, protocol-based, pure Swift database offering effortless persistence of any object

There are many libraries out there that aims to help developers easily create and use SQLite databases. Unfortunately developers still have to get bogged down in simple tasks such as writing table definitions and SQL queries. SwiftyDB automatically handles everything you don't want to spend your time doing.

Øyvind Grimnes 489 Sep 9, 2022