groue / grdbquery Goto Github PK
View Code? Open in Web Editor NEWThe SwiftUI companion for GRDB
License: MIT License
The SwiftUI companion for GRDB
License: MIT License
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.
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?
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?
I was wondering today whether you could move the syntax a bit closer to what's common in SwiftUI (e.g. with button styles). In the README/demo you have the following code:
@Query(PlayerRequest())
private var player: Player?
I thought it would be nice to write it like this:
@Query(.player)
private var player: Player?
To make this work, you need to add the following extension:
extension Queryable where Self == PlayerRequest {
static var player: Self { PlayerRequest() }
}
(Of course, you can do this for the other requests as well).
But I eventually figured out I had to add GRDBQuery as a package dependency to get the demos and test to build :)
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.
(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?
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:
GRDBQuery/Sources/GRDBQuery/Query.swift
Line 406 in 22df898
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:
realm/realm-swift#7908
and some posting on the apple developer forum so it is definitely not a problem which only affects GRDBQuery.
I am using a NavigationSplitView with 3 columns on macOS.
The NavigationSplitView is built like this:
struct ContentView: View {
@EnvironmentObject private var navigationState: NavigationState
var body: some View {
NavigationSplitView {
Sidebar()
} content: {
content
} detail: {
detail
}
}
private var content: some View {
ZStack {
if let selectedPredicate = navigationState.selectedPredicate {
BookmarkList(predicate: selectedPredicate)
.id(selectedPredicate.id)
#if os(macOS)
.frame(minWidth: 280)
#endif
} else {
PlaceholderView(text: "Please select a row")
}
}
}
private var detail: some View {
ZStack {
if let selectedBookmark = navigationState.selectedBookmark {
BookmarkDetailView(id: selectedBookmark.id)
.id(selectedBookmark.id)
} else {
PlaceholderView(text: "Select a bookmark")
}
}
}
}
In the content column of this split view a list of bookmarks is rendered. These bookmarks are fetched via @Query
. In the detail pane a toolbar is drawn with two buttons (show previous bookmark and show next bookmark). Unfortunately I am not quite sure how to implement those actions since in my detail pane only the selected bookmark is known but not the list of all available/possibly filtered bookmarks. I think I would need to know the list of available bookmarks in BookmarkDetailView. Would it be a viable solution to fetch the bookmarks again in BookmarkDetailView or am I missing something obvious? I also attached a screenshot here to make things more clear.
Here is my current solution but as written above I would like to make sure that I am not doing something stupid here by refetching the bookmarks from the db and whether there is an obvious better solution:
var bookmarks: [Bookmark] {
do {
return try appDatabase.reader.read { db in
switch navigationState.selectedPredicate {
case .all:
return try Bookmark.all().fetchAll(db)
case .unread:
return try Bookmark.filter(Column("isArchived") == false).fetchAll(db)
case .archived:
return try Bookmark.filter(Column("isArchived") == true).fetchAll(db)
case .starred:
return try Bookmark.filter(Column("isStarred") == true).fetchAll(db)
case .none:
return []
}
}
} catch {
return []
}
}
var bookmarkIndex: Int? {
bookmarks.firstIndex(of: bookmark)
}
var hasPreviousBookmark: Bool {
guard let bookmarkIndex else { return false }
return bookmarkIndex > 0
}
var hasNextBookmark: Bool {
guard let bookmarkIndex else { return false }
return bookmarkIndex < bookmarks.count - 1
}
private var previousButton: some View {
Button {
guard let bookmarkIndex, let previousBookmark = bookmarks[safe: bookmarkIndex - 1] else {
return
}
self.navigationState.selectedBookmark = previousBookmark
} label: {
Image(systemName: "chevron.up")
}
.keyboardShortcut("p", modifiers: [])
.disabled(!hasPreviousBookmark)
}
private var nextButton: some View {
Button {
guard let bookmarkIndex, let nextBookmark = bookmarks[safe: bookmarkIndex + 1] else {
return
}
self.navigationState.selectedBookmark = nextBookmark
} label: {
Image(systemName: "chevron.down")
}
.keyboardShortcut("n", modifiers: [])
.disabled(!hasNextBookmark)
}
I know this is probably not an issue with the library per-se. But since all the issues posted here are answered super quick, competent and with a clear solution I wondered if you could help me as well with this one.
Kind regards,
David
I was trying to use @query to feed the suggestedTokens parameter for a searchable list. Here is an example of what I was trying to accomplish:
import SwiftUI
import GRDB
import GRDBQuery
import Combine
public struct Tag : Hashable, Codable, Identifiable, Equatable {
private(set) public var id: Int64?
public var tag: String
}
extension Tag: TableRecord, FetchableRecord, MutablePersistableRecord {
enum Columns {
static let id = Column(CodingKeys.id)
static let tag = Column(CodingKeys.tag)
}
public mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
public struct TagQuery: Queryable {
public static var defaultValue: [Tag] { [] }
public func publisher(in repository: Repository) -> AnyPublisher<[Tag], Error> {
ValueObservation.tracking { db in
return try Tag.fetchAll(db)
}
.publisher(in: repository.reader, scheduling: .immediate)
.eraseToAnyPublisher()
}
}
struct Test: View {
var data : [String] = ["A", "B", "C"]
@Binding var searchTerm : String
@State private var tokens: [Tag] = []
@Query(TagQuery())
private var suggestedTokens: [Tag]
var body: some View {
List {
ForEach(data, id: \.self) { data in
Text(data)
}
}
.searchable(text: $searchTerm, tokens: $tokens, suggestedTokens: $suggestedTokens) {token in
Text(token.tag)
}
}
}
it fails to compile with this error:
Cannot convert value of type 'Query<TagQuery>.Wrapper' to expected argument type 'Binding<[Token]>'
Is there a way to use @query to feed the suggestedTokens list?
I have a has many relationship setup, but not understanding how to access the data within a SwiftUI View.
extension Race: TableRecord {
static var sessions = hasMany(Session.self, using: ForeignKey(["race_id"]))
var sessions: QueryInterfaceRequest<Session> {
return request(for: Race.sessions)
}
}
In my View I have
@Query(RaceRequest(ordering: .byStart)) private var races: [Race]
Then within the ForEach of races, I want to do a ForEach of sessions but am totally lost
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.
I've used GRDBQuery
on several projects now, it's a good package.
I endup pasting the following two helpers into all of my projects. I noticed that GRDBQuery doesn't depend on GRDB, so I can't directly open a pull request to share these. Writing about them here in case some one might find them useful, or might find a way to include them in either GRDB or GRDBQuery.
import GRDB
import GRDBQuery
import Combine
protocol OptionalQueryable: Queryable {
associatedtype V
func fetch(_ db: Database) throws -> V?
}
extension OptionalQueryable where DatabaseContext == DatabaseWriter, ValuePublisher == AnyPublisher<V?, Error> {
static var defaultValue: V? { nil }
func publisher(in database: DatabaseWriter) -> AnyPublisher<V?, Error> {
ValueObservation
.tracking(regions: [.fullDatabase], fetch: fetch)
.publisher(in: database, scheduling: .immediate)
.eraseToAnyPublisher()
}
}
protocol ListQueryable: Queryable {
associatedtype V
func fetch(_ db: Database) throws -> [V]
}
extension ListQueryable {
static var defaultValue: [V] { [] }
func publisher(in database: DatabaseWriter) -> AnyPublisher<[V], Error> {
ValueObservation
.tracking(fetch)
.publisher(in: database, scheduling: .immediate)
.eraseToAnyPublisher()
}
}
Where MyRecord
conforms to Codable
and FetchableRecord
, and where MyRecord.CodingKeys
conforms to ColumnExpression
.
extension MyRecord {
struct FetchByID: OptionalQueryable {
let id: MyRecord.ID
func fetch(_ db: Database) throws -> MyRecord? {
try MyRecord
.filter(MyRecord.CodingKeys.id == id)
.fetchOne(db)
}
}
}
Also, with these helpers, it's easy to write tests around the query.
final class MyQueryTest: XCTestCase {
func testMyQuery() throws {
let result = try AppDatabase.fixture().read { db in
try MyQuery().fetch(db)
}
XCTAssertEqual(result.count, 3)
}
}
Where AppDatabase.fixture()
is a DatabaseQueue
with the migrations run and some fixtures added.
Hi, I'm new to GRDB and GRDBQuery. I'm making an app using SwiftUI. The app has a list of items on a sidebar and details of the clicked item on the detail pane. But, I'm having a problem. When I click on a different item in the sidebar, the details don't change.
Here is the relevant code:
struct AppView: View {
@Environment(\.appDatabase) private var appDatabase
@Query(TeamRequest()) private var teams: [Team]
@State private var selectedTeam: Team?
var body: some View {
NavigationSplitView(sidebar: {
List(selection: $selectedTeam) {
ForEach(teams) { team in
NavigationLink(value: team) {
Text(team.name)
}
}
}
}) {
if let selectedTeam = selectedTeam {
PlayerList(team: selectedTeam)
} else {
Text("No team selected")
}
}
}
}
struct PlayerList: View {
@Environment(\.appDatabase) private var appDatabase
@Query<PlayerRequest> private var players: [Player]
private let team: Team
init(team: Team) {
self.team = team
_players = Query(PlayerRequest(team: team))
}
var body: some View {
List {
ForEach(players) { player in
Text(player.name)
}
}
.navigationTitle(team.name)
}
}
Can you help me understand what's wrong or how to fix it? Thank you for your time.
Say there is a @State private var search: String = ""
plus two @Queries
in a view:
@Query(RequestA(search: "")) private var resultA: A;
@Query(RequestB(search: "")) private var resultB: B;
These queries need to be synced with the $search
binding.
Besides the (imperative) .onChange(search) {...}
, is there a better (descriptive) way? Do I need to introduce an extra layer of view to construct the queries?
Thank you ❤️
The Queryable protocol conforms to the Equatable protocol, how should I implement it for a query which do not have parameters in the query (no where clause)? E.g.:
When opening the package in Xcode 14.3 beta (14E5197f), the build fails with error 'StateObject' is only available in [macOS 11.0 | iOS 14.0] or newer
.
Culprits are EnvironmentStateObject.swift line 253
and Query.swift line 172
.
This StackOverflow thread seems to indicate that in runtime, accessing StateObject
in iOS 13 would cause a exception to be raised, but I have not tested it myself.
Apple documentation says it is available in iOS 14.0+, macOS 11.0+, tvOS 14.0+, watchOS 7.0+
, so maybe we could get a new version cut with those versions as minimum in the Package file?
Thanks!
🙇
I'm running command to create release build and I get weird error, I wonder if you know what this could be?
Command: xcodebuild -scheme Gem -project ../wallet/ios/Gem.xcodeproj -destination
Logs:
2023-05-02 06:46:08.528 xcodebuild[13353:50500] [MT] IDEFileReferenceDebug: [Load] <IDESwiftPackageCore.IDESwiftPackageSpecialFolderFileReference, 0x6000024bb100: name:Documentation.docc path:group:Documentation.docc> Failed to load container at path: /Users/runner/Library/Developer/Xcode/DerivedData/Gem-dfmqdzwzfjnhdngoouvpgqvecrml/SourcePackages/checkouts/GRDB.swift/GRDB/Documentation.docc, Error: Error Domain=com.apple.dt.IDEContainerErrorDomain Code=6 "Cannot open "Documentation.docc" as a "Swift Package Folder" because it is already open as a "Folder"." UserInfo={NSLocalizedDescription=Cannot open "Documentation.docc" as a "Swift Package Folder" because it is already open as a "Folder".}
2023-05-02 06:46:08.534 xcodebuild[13353:50500] [MT] IDEFileReferenceDebug: [Load] <IDESwiftPackageCore.IDESwiftPackageSpecialFolderFileReference, 0x600002468200: name:Documentation.docc path:group:Documentation.docc> Failed to load container at path: /Users/runner/Library/Developer/Xcode/DerivedData/Gem-dfmqdzwzfjnhdngoouvpgqvecrml/SourcePackages/checkouts/GRDBQuery/Sources/GRDBQuery/Documentation.docc, Error: Error Domain=com.apple.dt.IDEContainerErrorDomain Code=6 "Cannot open "Documentation.docc" as a "Swift Package Folder" because it is already open as a "Folder"." UserInfo={NSLocalizedDescription=Cannot open "Documentation.docc" as a "Swift Package Folder" because it is already open as a "Folder".}
In the demo project, the Player
model is edited via transform in the updateScore
function. For a more involved situation, say - we wanted to edit Player
's name, I'm trying to grasp the relationship between Query
and mutations. I have it working in the demo app with this refactor to PlayerFormView
:
struct PlayerFormView: View {
@Environment(\.dbQueue) var dbQueue
@State private var player: Player = Player.placeholder
init(player:Player) {
_player = State(initialValue: player)
}
var body: some View {
VStack {
TextEditor(text: $player.name)
Stepper(
"Score: \(player.score)",
onIncrement: { player.score += 10 },
onDecrement: { player.score = max(0, player.score - 10) })
Spacer()
}
.onChange(of: player) { newValue in
update()
}
}
private func update() {
do {
_ = try dbQueue.write { db in
try player.save(db)
}
} catch PersistenceError.recordNotFound {
// Oops, player does not exist.
// Ignore this error: `PlayerPresenceView` will dismiss.
//
// You can comment out this specific handling of
// `PersistenceError.recordNotFound`, run the preview, change the
// score, and see what happens.
} catch {
fatalError("\(error)")
}
}
}
The changes are basically swapping var player: Player
for a @State
version and calling save
instead of updateChanges
. This is driven by making Player
conform to Equatable
and then leveraging onChange
to know when to save. Two questions:
Query
is not meant for mutations, but rather observing changes. Is this correct? That is to say, we couldn't use the @Query<PlayerPresenceRequest>
for a binding as well (I know this one is used to look for the status, but assume it simply vended a Player
type).Update
I guess another drawback with this is, if there is another window open - the @Binding var player
doesn't seem to update in the windows which are not in the foreground (the nav bar does, but the PlayerFormView
doesn't). This could also be an implementation error on my part:
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!
(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?
Given a request:
struct PlayerRequest: Queryable {
static var defaultValue: Player? { nil }
var id: Int
func publisher(in reader: DatabaseReader)
-> AnyPublisher<Player?, Error> {
ValueObservation
.tracking(Player.fetchOne(id: id))
.publisher(in: reader)
.eraseToAnyPublisher()
}
}
Let’s have a root view:
struct PlayerRoot: View {
@Query(PlayerRequest(id: 0))
private var player: Player?
var body: some View {
Text("Foo")
}
}
The Player.fetchOne
static function is invoked twice.
How can I avoid the 2nd unnecessary query?
Thanks!
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
Before | After |
---|---|
RPReplay_Final1722004164.MP4 |
RPReplay_Final1722004253.MP4 |
What I did:
Queryable
to ValueObservationQueryable
func fetchValue(_ db: Database) throws
to func fetch(_ db: Database) throws
.environment(\.appDatabase, AppDatabase.shared)
to .appDatabase(.shared)
Basically, things I need to do according to the migration guide.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.