Last active
June 18, 2024 06:17
-
-
Save juanarzola/3c078df306daf530daff952465136658 to your computer and use it in GitHub Desktop.
Proposed query API for SwiftData notifications (stream based)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Our one super simple SwiftData Model | |
@Model | |
class Item { | |
... | |
} | |
// MARK: - TodayView | |
@MainActor | |
struct TodayView: View { | |
@State private var controller = TodayController() | |
@Environment(\.modelContext) private var modelContext | |
var body: some View { | |
HStack { | |
switch controller.model { | |
case .loading(let prevValue): | |
// render loading states (loading/reloading) | |
... | |
case .done(let content) | |
TodayViewContent(content: content) | |
} | |
) | |
.task { | |
// await all updates in the controller | |
await controller.updates(modelContext) | |
} | |
} | |
} | |
// MARK: - TodayViewModel | |
@MainActor | |
@Observable final class TodayController { | |
enum LoadError: Error {} | |
struct Model { | |
let sections: [TodaySection] | |
} | |
// some data that the view consumes (just a single model for now, but it's ok to have more than 1 property of data here, we are not trying to be MVC ;)) | |
private(set) var model: Loadable<Model, LoadError> = .notStarted | |
// a stream of updates that the controller handles internally. The view calls the update(with:) function instead. | |
@ObservationIgnored private var updates: AsyncStream<[Item]>? = nil | |
/// Main function used by the View to observe updates in a task. It updates model whenever queries (and in the future, any other relevant inputs) change. | |
func updates(_ modelContext: ModelContext) async { | |
let updates = self.updates ?? Self.createUpdatesSequence(in: modelContext, isInitialLoad: true) | |
self.updates = updates | |
for await items in updates { | |
await load(with: items) | |
} | |
// this code hits when the view's task is cancelled. We want to keep observing (buffering the last update) after the task is cancelled, but set isInitialLoad to false so that we don't get an extra unnecessary update. | |
self.updates = Self.createUpdatesSequence(in: modelContext, isInitialLoad: false) | |
} | |
func load(with queryData: [Items]) async { | |
self.model = .loading(previousValue: self.content.value) | |
// build sections, then set the content to .done. The sections are structs built from SwiftData's result and are what the View displays | |
let sections = await Self.sections(from: queryData) | |
self.model = .done(.init(sections: sections)) | |
} | |
// MARK: - Controller Updates Stream | |
/// This is where we use the proposed Query `updates` API to create and observe queries. | |
/// If we were to support more than 1 query in this controller, this stream can be replaced by an enum | |
/// for each type of update and use AsyncAlgorithms's `merge` to merge all the query streams) | |
private static func createUpdatesStream(in modelContext: ModelContext, isInitialLoad: Bool) -> AsyncStream<[Item]> { | |
// build the query | |
let descriptor = ... | |
let query = Query(descriptor, modelContext: modelContext) | |
// return the stream of the query | |
// if withCurrentValue is true the stream returns the current value, then observes subsequent ones. If false, it only observes subsequent ones. | |
// In order to properly implement this under the hood, SwiftData's stream needs a buffer size of 1 to always remember the latest value. | |
return query.updates(withCurrentValue: isInitialLoad) | |
} | |
// MARK: - Section building | |
private static func sections(from: [Items]) async -> [Section] { | |
// build sections from SwiftData Items | |
// it could be async (data loaded from an actor) or not, depends on your use case | |
... | |
return sections | |
} | |
} |
This Query macro demonstrates a similar idea as well (using a stored property in addition to observing) https://github.com/juanarzola/Queried
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why would you like to use Query outside the view? Imagine that
sections(from: [Items])
is expensive because it realizes many faults in the Items array. Here, it only executes once per query update.In view-based @query macros, it would have to be called in the body of the view, for every state change of the view, regardless of whether or not the query's dependencies changed, which is not great.