From 2baca641140b428680416ddbdfc4800c096c2b4c Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Tue, 31 Dec 2024 14:46:43 -0500 Subject: [PATCH] Adding documentation for the new onStoreDidLoad methods --- .../Articles/Using Stores.md | 53 +++++++++++++++++++ Sources/Boutique/Store+Observation.swift | 12 +++++ 2 files changed, 65 insertions(+) diff --git a/Sources/Boutique/Documentation.docc/Articles/Using Stores.md b/Sources/Boutique/Documentation.docc/Articles/Using Stores.md index 5b6445b..3e3b116 100644 --- a/Sources/Boutique/Documentation.docc/Articles/Using Stores.md +++ b/Sources/Boutique/Documentation.docc/Articles/Using Stores.md @@ -114,6 +114,59 @@ func getItems() async -> [Item] { The synchronous initializer is a sensible default, but if your app's needs dictate displaying data only once you've loaded all of the necessary items the asynchronous initializers are there to help. +## Observing Store Loading State + +You can manually observe the loading state of a ``Store`` as we do in `getItems()` above, but Boutique also provides a ``onStoreDidLoad`` function to observe the loading state of a ``Store`` in SwiftUI. + +```swift +struct ContentView: View { + @Stored var items: [Item] + @State var itemsHaveLoaded = false + + var body: some View { + VStack { + AlwaysVisibleBanner() + + if self.itemsHaveLoaded { + if self.items.isEmpty { + EmptyStateView() + } else { + ItemsView(items: self.items) + } + } else { + LoadingStateView() + } + } + .onStoreDidLoad( + self.$items, + update: $itemsHaveLoaded, + onError: { error in + log.error("Failed to load items", error) + } + ) + } +} +``` + +This allows for a clean separation of Views to display across three different states: +- When the Store has finished loading and has items +- When the Store has finished loading and has no items +- When the Store is loading (and implicitly has no items) + +You can also choose to use the closure-oriented variant of ``onStoreDidLoad`` to perform any additional logic when the ``Store`` has finished loading. Patterns like MVVM choose to isolate this logic ViewModel, and you can still choose to do that, but exposing this method on a View provides more flexibility to work with your preferred architecture. In the example below we will filter the items in a ``Store`` based on some criteria, to display only the relevant items in our View. + +```swift +.onStoreDidLoad( + self.$items, + onLoad: { + self.items = self.filteredItems(self.items) + }, + onError: { error in + log.error("Failed to load items", error) + } +) +``` + ## Further Exploration, @Stored And More Building an app using the ``Store`` can be really powerful because it leans into SwiftUI's state-driven architecture, while providing you with offline-first capabilities, realtime updates across your app, with almost no additional code required. diff --git a/Sources/Boutique/Store+Observation.swift b/Sources/Boutique/Store+Observation.swift index a9df2b5..ef9e5a9 100644 --- a/Sources/Boutique/Store+Observation.swift +++ b/Sources/Boutique/Store+Observation.swift @@ -2,6 +2,12 @@ import SwiftUI public extension View { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + /// Observes whether a ``Store`` has finished loading it's items. + /// + /// - Parameters: + /// - store: The ``Store`` whose loading is being observed. + /// - onLoad: The closure to call when the ``Store`` has finished loading. + /// - onError: The closure to call if an error occurs and the ``Store`` is not loaded. func onStoreDidLoad(_ store: Store, onLoad: @escaping () -> Void, onError: ((Error) -> Void)? = nil) -> some View { self.task({ do { @@ -14,6 +20,12 @@ public extension View { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + /// Observes whether a ``Store`` has finished loading it's items. + /// + /// - Parameters: + /// - store: The ``Store`` whose loading is being observed. + /// - update: A binding to that will be updated when the ``Store`` has finished loading. + /// - onError: The closure to call if an error occurs and the ``Store`` is not loaded. func onStoreDidLoad(_ store: Store, update hasLoadedState: Binding, onError: ((Error) -> Void)? = nil) -> some View { self.task({ do {