From 35bcb26be4396cccd1c611e502f548e77d5ba524 Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Mon, 19 Dec 2022 15:24:24 -0500 Subject: [PATCH] Updating Store documentation and adding a bit more code consistency --- .../Articles/Using Stores.md | 17 +++---- Sources/Boutique/Store+Identifiable.swift | 46 ++++++++++--------- Sources/Boutique/Store.swift | 43 +++++++++-------- Sources/Boutique/StoredValue+Dictionary.swift | 1 - Tests/BoutiqueTests/BoutiqueItem.swift | 7 +-- Tests/BoutiqueTests/StoredTests.swift | 25 +++++----- Tests/BoutiqueTests/StoredValueTests.swift | 1 + 7 files changed, 77 insertions(+), 63 deletions(-) diff --git a/Sources/Boutique/Documentation.docc/Articles/Using Stores.md b/Sources/Boutique/Documentation.docc/Articles/Using Stores.md index 51ff272..6c3da82 100644 --- a/Sources/Boutique/Documentation.docc/Articles/Using Stores.md +++ b/Sources/Boutique/Documentation.docc/Articles/Using Stores.md @@ -87,11 +87,12 @@ try await store ## Sync or Async? -To work with @``Stored`` or other 3rd party property wrappers, the ``Store`` has to be initialized synchronously, which means that the `items` are loaded in the background. However this can be an issue if you are using the ``Store`` directly and need to show the content on launch. The ``Store`` provides you with two options to handle this: +To work with @``Stored`` or alternative property wrappers the ``Store`` must be initialized synchronously. This means that the `items` of your ``Store`` will be loaded in the background, and may not be available immediately. However this can be an issue if you are using the ``Store`` directly and need to show the contents of the ``Store`` immediately, such as on your app's launch'. The ``Store`` provides you with two options to handle a scenario like this. + +By using the `async` overload of the ``Store`` initializer your ``Store`` will be returned once all of the `items` are loaded. -By using the `async` overload of the `init`, which returns the store once the `items` are loaded. ```swift -let store: Store +let store: Store init() async throws { store = try await Store(...) @@ -100,18 +101,18 @@ init() async throws { } ``` -Or you could still use the regular synchronous `Store.init` and then await for items to load before accessing them: +Alternatively you can use the synchronous initializer, and then await for items to load before accessing them. + ```swift -let store: Store = Store(...) +let store: Store = Store(...) -func getItems() async -> [YourItem] { +func getItems() async -> [Item] { try await store.itemsHaveLoaded() return await store.items } ``` -There is no "preferred" approach here, these tools are simply there to help you use the ``Store`` regardless of _how_ you use it. - +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. ## Further Exploration, @Stored And More diff --git a/Sources/Boutique/Store+Identifiable.swift b/Sources/Boutique/Store+Identifiable.swift index 63b3f05..f98a69b 100644 --- a/Sources/Boutique/Store+Identifiable.swift +++ b/Sources/Boutique/Store+Identifiable.swift @@ -4,13 +4,15 @@ public extension Store where Item: Identifiable, Item.ID == String { /// Initializes a new ``Store`` for persisting items to a memory cache and a storage engine, acting as a source of truth. /// - /// The items will be loaded asynchronously in a background task. If you are not using this with - /// `Stored` and need to show the content of the Store right away, you have two options: + /// The ``items`` will be loaded asynchronously in a background task. + /// If you are not using this with @``Stored`` and need to show + /// the contents of the Store right away, you have two options. + /// + /// - Move the ``Store`` initialization to an `async` context + /// so `init` returns only once items have been loaded. /// - /// - Move the Store initialization to an `async` context, so the `Store.init` returns only - /// once items have been loaded: /// ``` - /// let store: Store + /// let store: Store /// /// init() async throws { /// store = try await Store(...) @@ -19,11 +21,13 @@ public extension Store where Item: Identifiable, Item.ID == String { /// } /// ``` /// - /// - Wait for items to be loaded before accessing them: + /// - Alternatively you can use the synchronous initializer + /// and then await for items to load before accessing them. + /// /// ``` - /// let store: Store = Store(...) + /// let store: Store = Store(...) /// - /// func getItems() async -> [YourItem] { + /// func getItems() async -> [Item] { /// try await store.itemsHaveLoaded() /// return await store.items /// } @@ -38,10 +42,6 @@ public extension Store where Item: Identifiable, Item.ID == String { } /// Initializes a new ``Store`` for persisting items to a memory cache and a storage engine, acting as a source of truth, and await for the ``items`` to load. - /// - /// This initializer eschews providing a `cacheIdentifier` when our `Item` conforms to `Identifiable` - /// with an `id` that is a `String`. While it's not required for your `Item` to conform to `Identifiable`, - /// many SwiftUI-related objects do so this initializer provides a nice convenience. /// - Parameter storage: A `StorageEngine` to initialize a ``Store`` instance with. convenience init(storage: StorageEngine) async throws { try await self.init(storage: storage, cacheIdentifier: \.id) @@ -52,13 +52,15 @@ public extension Store where Item: Identifiable, Item.ID == UUID { /// Initializes a new ``Store`` for persisting items to a memory cache and a storage engine, acting as a source of truth. /// - /// The items will be loaded asynchronously in a background task. If you are not using this with - /// `Stored` and need to show the content of the Store right away, you have two options: + /// The ``items`` will be loaded asynchronously in a background task. + /// If you are not using this with @``Stored`` and need to show + /// the contents of the Store right away, you have two options. + /// + /// - Move the ``Store`` initialization to an `async` context + /// so `init` returns only once items have been loaded. /// - /// - Move the Store initialization to an `async` context, so the `Store.init` returns only - /// once items have been loaded: /// ``` - /// let store: Store + /// let store: Store /// /// init() async throws { /// store = try await Store(...) @@ -67,11 +69,13 @@ public extension Store where Item: Identifiable, Item.ID == UUID { /// } /// ``` /// - /// - Wait for items to be loaded before accessing them: + /// - Alternatively you can use the synchronous initializer + /// and then await for items to load before accessing them. + ///  /// ``` - /// let store: Store = Store(...) - /// - /// func getItems() async -> [YourItem] { + /// let store: Store = Store(...) + /// + /// func getItems() async -> [Item] { /// try await store.itemsHaveLoaded() /// return await store.items /// } diff --git a/Sources/Boutique/Store.swift b/Sources/Boutique/Store.swift index 3639e1c..d607da4 100644 --- a/Sources/Boutique/Store.swift +++ b/Sources/Boutique/Store.swift @@ -54,23 +54,19 @@ public final class Store: ObservableObject { /// or subscribe to it however they wish, but you desire making modifications to ``items`` /// you must use ``insert(_:)-7z2oe``, ``remove(_:)-3nzlq``, or ``removeAll()-9zfmy``. @MainActor @Published public private(set) var items: [Item] = [] - - private lazy var loadStoreTask: Task = Task { @MainActor in - let decoder = JSONDecoder() - self.items = try await self.storageEngine.readAllData() - .map({ try decoder.decode(Item.self, from: $0) }) - } /// Initializes a new ``Store`` for persisting items to a memory cache /// and a storage engine, to act as a source of truth. /// - /// The items will be loaded asynchronously in a background task. If you are not using this with - /// `Stored` and need to show the content of the Store right away, you have two options: + /// The ``items`` will be loaded asynchronously in a background task. + /// If you are not using this with @``Stored`` and need to show + /// the contents of the Store right away, you have two options. + /// + /// - Move the ``Store`` initialization to an `async` context + /// so `init` returns only once items have been loaded. /// - /// - Move the Store initialization to an `async` context, so the `Store.init` returns only - /// once items have been loaded: /// ``` - /// let store: Store + /// let store: Store /// /// init() async throws { /// store = try await Store(...) @@ -79,11 +75,13 @@ public final class Store: ObservableObject { /// } /// ``` /// - /// - Wait for items to be loaded before accessing them: + /// - Alternatively you can use the synchronous initializer + /// and then await for items to load before accessing them. + /// /// ``` - /// let store: Store = Store(...) + /// let store: Store = Store(...) /// - /// func getItems() async -> [YourItem] { + /// func getItems() async -> [Item] { /// try await store.itemsHaveLoaded() /// return await store.items /// } @@ -96,7 +94,9 @@ public final class Store: ObservableObject { public init(storage: StorageEngine, cacheIdentifier: KeyPath) { self.storageEngine = storage self.cacheIdentifier = cacheIdentifier - _ = self.loadStoreTask // Start the lazy task in the background. + + // Begin loading items in the background. + _ = self.loadStoreTask } /// Initializes a new ``Store`` for persisting items to a memory cache @@ -113,10 +113,10 @@ public final class Store: ObservableObject { try await itemsHaveLoaded() } - /// Awaits for `items` to be loaded. + /// Awaits for ``items`` to be loaded. /// - /// When initializing a `Store` in a non-async context, the items are loaded in a background task. - /// This functions provides a way to `await` its completion before accessing the `items`. + /// When initializing a ``Store`` in a non-async context, the items are loaded in a background task. + /// This functions provides a way to `await` its completion before accessing the ``items``. public func itemsHaveLoaded() async throws { try await loadStoreTask.value } @@ -300,6 +300,13 @@ public final class Store: ObservableObject { try await self.performRemoveAll() } + /// A `Task` that will kick off loading items into the ``Store``. + private lazy var loadStoreTask: Task = Task { @MainActor in + let decoder = JSONDecoder() + self.items = try await self.storageEngine.readAllData() + .map({ try decoder.decode(Item.self, from: $0) }) + } + } #if DEBUG diff --git a/Sources/Boutique/StoredValue+Dictionary.swift b/Sources/Boutique/StoredValue+Dictionary.swift index 20e274d..cc61661 100644 --- a/Sources/Boutique/StoredValue+Dictionary.swift +++ b/Sources/Boutique/StoredValue+Dictionary.swift @@ -44,5 +44,4 @@ public extension AsyncStoredValue { try await self.set(updatedDictionary) } - } diff --git a/Tests/BoutiqueTests/BoutiqueItem.swift b/Tests/BoutiqueTests/BoutiqueItem.swift index 3ce4a10..1fff748 100644 --- a/Tests/BoutiqueTests/BoutiqueItem.swift +++ b/Tests/BoutiqueTests/BoutiqueItem.swift @@ -1,13 +1,15 @@ import Foundation struct BoutiqueItem: Codable, Equatable, Identifiable { - var id: String { merchantID } + var id: String { + self.merchantID + } + let merchantID: String let value: String } extension BoutiqueItem { - static let coat = BoutiqueItem( merchantID: "1", value: "Coat" @@ -47,5 +49,4 @@ extension BoutiqueItem { BoutiqueItem.purse, BoutiqueItem.belt, ] - } diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index 0af080b..a2c6155 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -3,26 +3,25 @@ import Combine import XCTest extension Store where Item == BoutiqueItem { - static let boutiqueItemStore = Store( - storage: SQLiteStorageEngine.default(appendingPath: "StoredTests") - ) + static let boutiqueItemsStore = Store( + storage: SQLiteStorageEngine.default(appendingPath: "StoredTests") + ) } final class StoredTests: XCTestCase { - - @Stored(in: .boutiqueItemStore) private var items - + + @Stored(in: .boutiqueItemsStore) private var items + private var cancellables: Set = [] override func setUp() async throws { try await $items.itemsHaveLoaded() try await $items.removeAll() } - - override func tearDown() { - cancellables.removeAll() - } + override func tearDown() { + cancellables.removeAll() + } @MainActor func testInsertingItem() async throws { @@ -52,7 +51,7 @@ final class StoredTests: XCTestCase { @MainActor func testReadingItems() async throws { try await $items.insert(BoutiqueItem.allItems) - + XCTAssertEqual(items[0], BoutiqueItem.coat) XCTAssertEqual(items[1], BoutiqueItem.sweater) XCTAssertEqual(items[2], BoutiqueItem.purse) @@ -68,7 +67,8 @@ final class StoredTests: XCTestCase { // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "StoredTests"), - cacheIdentifier: \.merchantID) + cacheIdentifier: \.merchantID + ) XCTAssertEqual(newStore.items.count, 4) @@ -239,3 +239,4 @@ final class StoredTests: XCTestCase { wait(for: [expectation], timeout: 1) } } + diff --git a/Tests/BoutiqueTests/StoredValueTests.swift b/Tests/BoutiqueTests/StoredValueTests.swift index 59df221..0c1aef9 100644 --- a/Tests/BoutiqueTests/StoredValueTests.swift +++ b/Tests/BoutiqueTests/StoredValueTests.swift @@ -115,3 +115,4 @@ final class StoredValueTests: XCTestCase { } } +