Skip to content

Commit 0aba45e

Browse files
Introduce FetchTask to tie database observation to view lifetime (#295)
* Support tasks for loading query. * wip * wip * new test * wip * wip * wip * Add @discardableResult to load functions * Added a migration guide. --------- Co-authored-by: Brandon Williams <mbrandonw@hey.com>
1 parent fe981a7 commit 0aba45e

13 files changed

Lines changed: 346 additions & 63 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ jobs:
1414
name: macOS
1515
strategy:
1616
matrix:
17-
xcode: ['16.4']
17+
xcode: ['26.1']
1818
config: ['debug', 'release']
19-
runs-on: macos-15
19+
runs-on: macos-26
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v5
2222
- name: Select Xcode ${{ matrix.xcode }}
2323
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
2424
- name: Run ${{ matrix.config }} tests
@@ -28,13 +28,13 @@ jobs:
2828
name: Examples
2929
strategy:
3030
matrix:
31-
xcode: ['16.4']
31+
xcode: ['26.1']
3232
config: ['debug']
3333
scheme: ['Reminders', 'CaseStudies', 'SyncUps']
34-
runs-on: macos-15
34+
runs-on: macos-26
3535
continue-on-error: true
3636
steps:
37-
- uses: actions/checkout@v4
37+
- uses: actions/checkout@v5
3838
- name: Select Xcode ${{ matrix.xcode }}
3939
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
4040
- name: List devices available

Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/Reminders/ReminderForm.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ struct ReminderFormView: View {
122122
}
123123
.task(id: reminder.remindersListID) {
124124
await withErrorReporting {
125-
try await $remindersList.load(RemindersList.find(reminder.remindersListID))
125+
try await $remindersList.load(RemindersList.find(reminder.remindersListID)).task
126126
}
127127
}
128128
}

Examples/Reminders/Schema.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,14 @@ nonisolated private let logger = Logger(subsystem: "Reminders", category: "Datab
387387
func seedSampleData() throws {
388388
@Dependency(\.date.now) var now
389389
@Dependency(\.uuid) var uuid
390-
let remindersListIDs = (0...2).map { _ in uuid() }
391-
let reminderIDs = (0...10).map { _ in uuid() }
390+
var remindersListIDs: [UUID] = []
391+
for _ in 0...2 {
392+
remindersListIDs.append(uuid())
393+
}
394+
var reminderIDs: [UUID] = []
395+
for _ in 0...10 {
396+
reminderIDs.append(uuid())
397+
}
392398
try seed {
393399
RemindersList(
394400
id: remindersListIDs[0],

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ let package = Package(
3232
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"),
3333
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"),
3434
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"),
35+
.package(url: "https://github.com/pointfreeco/swift-perception", from: "2.0.0"),
3536
.package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"),
3637
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"),
3738
.package(
@@ -53,6 +54,7 @@ let package = Package(
5354
.product(name: "GRDB", package: "GRDB.swift"),
5455
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
5556
.product(name: "OrderedCollections", package: "swift-collections"),
57+
.product(name: "Perception", package: "swift-perception"),
5658
.product(name: "Sharing", package: "swift-sharing"),
5759
.product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"),
5860
.product(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Migration guides
2+
3+
Learn how to upgrade your application to the latest version of SQLiteData.
4+
5+
## Overview
6+
7+
SQLiteData is under constant development, and we are always looking for ways to simplify the
8+
library and make it more powerful. As such, we often need to deprecate certain APIs in favor of
9+
newer ones. We recommend people update their code as quickly as possible to the newest APIs, and
10+
these guides contain tips to do so.
11+
12+
> Important: Before following any particular migration guide be sure you have followed all the
13+
> preceding migration guides.
14+
15+
## Topics
16+
17+
- <doc:MigratingTo1.4>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Migrating to 1.4
2+
3+
SQLiteData 1.4 introduces a new tool for tying the lifecycle database subscriptions to the
4+
lifecycle of the surrounding async context, but it may incidentally cause "Result of call …
5+
is unused" warnings in your project.
6+
7+
## Overview
8+
9+
The `load` method defined on [`@FetchAll`](<doc:FetchAll>) / [`@FetchOne`](<doc:FetchOne>) /
10+
[`@Fetch`](<doc:Fetch>) all now return a discardable result, ``FetchSubscription``. Awaiting the
11+
``FetchSubscription/task`` of that result ties the lifecycle of the subscription to the database
12+
to the lifecycle of the surrounding async context, which can help views to automatically
13+
unsubscribe from the database when they are not visible.
14+
15+
However, when used with `withErrorReporting` you are likely to get the following warning:
16+
17+
```swift
18+
private func updateQuery() async {
19+
// ⚠️ Result of call to 'withErrorReporting(_:to:fileID:filePath:line:column:isolation:catching:)' is unused
20+
await withErrorReporting {
21+
try await $rows.load()
22+
}
23+
}
24+
```
25+
26+
This is happening because although `load` has a discardable result, Swift does not propagate that
27+
to `withErrorReporting`, and so Swift thinks you have an unused value. To fix you will need to
28+
explicitly ignore the result with `_ = `:
29+
30+
```swift
31+
private func updateQuery() async {
32+
_ = await withErrorReporting {
33+
try await $rows.load()
34+
}
35+
}
36+
```

Sources/SQLiteData/Documentation.docc/SQLiteData.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ with SQLite to take full advantage of GRDB and SQLiteData.
309309
- ``FetchAll``
310310
- ``FetchOne``
311311
- ``Fetch``
312+
- ``FetchSubscription``
312313

313314
### CloudKit synchronization and sharing
314315

Sources/SQLiteData/Fetch.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,14 @@ public struct Fetch<Value: Sendable>: Sendable {
9898
/// - request: A request describing the data to fetch.
9999
/// - database: The database to read from. A value of `nil` will use the default database
100100
/// (`@Dependency(\.defaultDatabase)`).
101+
/// - Returns: A subscription associated with the observation.
102+
@discardableResult
101103
public func load(
102104
_ request: some FetchKeyRequest<Value>,
103105
database: (any DatabaseReader)? = nil
104-
) async throws {
106+
) async throws -> FetchSubscription {
105107
try await sharedReader.load(.fetch(request, database: database))
108+
return FetchSubscription(sharedReader: sharedReader)
106109
}
107110
}
108111

@@ -136,12 +139,15 @@ extension Fetch {
136139
/// (`@Dependency(\.defaultDatabase)`).
137140
/// - scheduler: The scheduler to observe from. By default, database observation is performed
138141
/// asynchronously on the main queue.
142+
/// - Returns: A subscription associated with the observation.
143+
@discardableResult
139144
public func load(
140145
_ request: some FetchKeyRequest<Value>,
141146
database: (any DatabaseReader)? = nil,
142147
scheduler: some ValueObservationScheduler & Hashable
143-
) async throws {
148+
) async throws -> FetchSubscription {
144149
try await sharedReader.load(.fetch(request, database: database, scheduler: scheduler))
150+
return FetchSubscription(sharedReader: sharedReader)
145151
}
146152
}
147153

@@ -193,13 +199,16 @@ extension Fetch: Equatable where Value: Equatable {
193199
/// (`@Dependency(\.defaultDatabase)`).
194200
/// - animation: The animation to use for user interface changes that result from changes to
195201
/// the fetched results.
202+
/// - Returns: A subscription associated with the observation.
196203
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
204+
@discardableResult
197205
public func load(
198206
_ request: some FetchKeyRequest<Value>,
199207
database: (any DatabaseReader)? = nil,
200208
animation: Animation
201-
) async throws {
209+
) async throws -> FetchSubscription {
202210
try await sharedReader.load(.fetch(request, database: database, animation: animation))
211+
return FetchSubscription(sharedReader: sharedReader)
203212
}
204213
}
205214
#endif

0 commit comments

Comments
 (0)