Last active
June 8, 2024 17:31
-
-
Save juanarzola/d7225f17bffc62b941ed3f6a1317dfe2 to your computer and use it in GitHub Desktop.
ViewModel (or controller(!)) updateLoop pattern that keeps the model up-to-date in a task.
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
// MARK: - TodayView | |
@MainActor | |
struct TodayView: View { | |
@State private var viewModel = TodayViewModel() | |
@Environment(\.modelContext) private var modelContext | |
var body: some View { | |
HStack { | |
switch viewModel.content { | |
case .loading(let prevValue): | |
// render loading states (loading/reloading) | |
... | |
case .done(let content) | |
TodayViewContent(content: content) | |
} | |
) | |
// an async loop that keeps viewModel up-to-date as a result of external events (can change | |
// its content state from loading/done) | |
// | |
// the viewModel ensures that updates are still being generated even after | |
// the task ends - next time the task executes (when TodayView is re-added to the hierarchy) | |
// it'll receive any buffered events. | |
.task(priority: .userInitiated) { | |
await viewModel.updateLoop(modelContext.container) | |
} | |
} | |
} | |
// MARK: - TodayViewModel | |
@MainActor | |
@Observable final class TodayViewModel { | |
enum LoadError: Error {} | |
struct Content { | |
let dayStats: DayStats? | |
let someSection: EventGroupsSection? | |
let anotherSection: EventGroupsSection? | |
} | |
private(set) var content: Loadable<Content, LoadError> = .notStarted | |
init() { | |
self.updates = Self.createUpdatesSequence(isInitialLoad: true) | |
} | |
/// Update content and listen for updates. Use in a task in your view to listen for updates | |
func updateLoop(_ container: ModelContainer) async { | |
let actor = StudyStatsActor(modelContainer: container) | |
for await _ in updates { | |
await load(with: actor) | |
} | |
// keep observing (buffering the last update) after the task is cancelled | |
self.updates = Self.createUpdatesSequence(isInitialLoad: false) | |
} | |
func load(with statsActor: StudyStatsActor, calendar: Calendar = Calendar.current) async { | |
// set to loading (or reloading, if previous value is not nil) | |
self.content = .loading(previousValue: self.content.value) | |
let thisMonth = calendar.dateComponents([.month, .year], from: Date.now) | |
let monthStats = await statsActor.stats(forMonth: thisMonth) | |
// build day stats and sections, then set the content to .done | |
... | |
... | |
// why animate here? because animation() or transaction with value of content in the view doesn't animate. | |
withAnimation { | |
self.content = .done( | |
.init( | |
dayStats: dayStats, | |
someSection: someSection, | |
anotherSection: anotherSection | |
) | |
) | |
} | |
} | |
// MARK: - Update streams | |
private static func createUpdatesSequence(isInitialLoad: Bool) -> AsyncMerge3Sequence<AsyncStream<Void>, AsyncStream<Void>, AsyncStream<Void>> { | |
let initialLoad = AsyncStream<Void>(bufferingPolicy: .bufferingNewest(1)) { continuation in | |
// Generate an item in the initial load stream only on initial load. | |
// This allows `updateLoop` to iterate at least once the first time it's awaited by the client. | |
// Future awaits won't include this initial load item. | |
if isInitialLoad { | |
continuation.yield() | |
} | |
continuation.finish() | |
} | |
let allUpdates = merge( | |
initialLoad, | |
coreDataUpdates, | |
significantTimeChangeUpdates | |
) | |
return allUpdates | |
} | |
private static var coreDataUpdates: AsyncStream<Void> { | |
AsyncStream<Void>(bufferingPolicy: .bufferingNewest(1)) { continuation in | |
Task { | |
// observe updates and continuation.yield() on the relevant ones | |
... | |
} | |
} | |
} | |
private static var significantTimeChangeUpdates: AsyncStream<Void> { | |
AsyncStream<Void>(bufferingPolicy: .bufferingNewest(1)) { continuation in | |
Task { | |
// observes significant time update notifications | |
... | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment