Skip to content

Commit

Permalink
Updating Store documentation and adding a bit more code consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
mergesort committed Dec 19, 2022
1 parent 3f46be9 commit 35bcb26
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 63 deletions.
17 changes: 9 additions & 8 deletions Sources/Boutique/Documentation.docc/Articles/Using Stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<YourItem>
let store: Store<Item>

init() async throws {
store = try await Store(...)
Expand All @@ -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<YourItem> = Store(...)
let store: Store<Item> = 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

Expand Down
46 changes: 25 additions & 21 deletions Sources/Boutique/Store+Identifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<YourItem>
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
Expand All @@ -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<YourItem> = Store(...)
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [YourItem] {
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
Expand All @@ -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)
Expand All @@ -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<YourItem>
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
Expand All @@ -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<YourItem> = Store(...)
///
/// func getItems() async -> [YourItem] {
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
Expand Down
43 changes: 25 additions & 18 deletions Sources/Boutique/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,19 @@ public final class Store<Item: Codable & Equatable>: 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<Void, Error> = 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<YourItem>
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
Expand All @@ -79,11 +75,13 @@ public final class Store<Item: Codable & Equatable>: 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<YourItem> = Store(...)
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [YourItem] {
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
Expand All @@ -96,7 +94,9 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
public init(storage: StorageEngine, cacheIdentifier: KeyPath<Item, String>) {
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
Expand All @@ -113,10 +113,10 @@ public final class Store<Item: Codable & Equatable>: 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
}
Expand Down Expand Up @@ -300,6 +300,13 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
try await self.performRemoveAll()
}

/// A `Task` that will kick off loading items into the ``Store``.
private lazy var loadStoreTask: Task<Void, Error> = Task { @MainActor in
let decoder = JSONDecoder()
self.items = try await self.storageEngine.readAllData()
.map({ try decoder.decode(Item.self, from: $0) })
}

}

#if DEBUG
Expand Down
1 change: 0 additions & 1 deletion Sources/Boutique/StoredValue+Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,4 @@ public extension AsyncStoredValue {
try await self.set(updatedDictionary)
}


}
7 changes: 4 additions & 3 deletions Tests/BoutiqueTests/BoutiqueItem.swift
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -47,5 +49,4 @@ extension BoutiqueItem {
BoutiqueItem.purse,
BoutiqueItem.belt,
]

}
25 changes: 13 additions & 12 deletions Tests/BoutiqueTests/StoredTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,25 @@ import Combine
import XCTest

extension Store where Item == BoutiqueItem {
static let boutiqueItemStore = Store<BoutiqueItem>(
storage: SQLiteStorageEngine.default(appendingPath: "StoredTests")
)
static let boutiqueItemsStore = Store<BoutiqueItem>(
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<AnyCancellable> = []

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 {
Expand Down Expand Up @@ -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)
Expand All @@ -68,7 +67,8 @@ final class StoredTests: XCTestCase {
// The new store has to fetch items from disk.
let newStore = try await Store<BoutiqueItem>(
storage: SQLiteStorageEngine.default(appendingPath: "StoredTests"),
cacheIdentifier: \.merchantID)
cacheIdentifier: \.merchantID
)

XCTAssertEqual(newStore.items.count, 4)

Expand Down Expand Up @@ -239,3 +239,4 @@ final class StoredTests: XCTestCase {
wait(for: [expectation], timeout: 1)
}
}

1 change: 1 addition & 0 deletions Tests/BoutiqueTests/StoredValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ final class StoredValueTests: XCTestCase {
}

}

0 comments on commit 35bcb26

Please sign in to comment.