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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This Query macro demonstrates a similar idea as well (using a stored property in addition to observing) https://github.com/juanarzola/Queried