Skip to content

Commit

Permalink
Merge pull request #39 from rl-pavel/add-async-init
Browse files Browse the repository at this point in the history
Add `init(...) async throws` overloads to the `Store`.
  • Loading branch information
mergesort authored Jan 8, 2023
2 parents 6bbaa4f + 35bcb26 commit ad47507
Show file tree
Hide file tree
Showing 9 changed files with 678 additions and 19 deletions.
30 changes: 30 additions & 0 deletions Sources/Boutique/Documentation.docc/Articles/Using Stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,36 @@ try await store
.run()
```


## Sync or Async?

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.

```swift
let store: Store<Item>

init() async throws {
store = try await Store(...)
// Now the store will have `items` already loaded.
let items = await store.items
}
```

Alternatively you can use the synchronous initializer, and then await for items to load before accessing them.

```swift
let store: Store<Item> = Store(...)

func getItems() async -> [Item] {
try await store.itemsHaveLoaded()
return await store.items
}
```

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

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.
Expand Down
75 changes: 73 additions & 2 deletions Sources/Boutique/Store+Identifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,97 @@ 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 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.
///
/// ```
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
/// // Now the store will have `items` already loaded.
/// let items = await store.items
/// }
/// ```
///
/// - Alternatively you can use the synchronous initializer
/// and then await for items to load before accessing them.
///
/// ```
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
/// ```
///
/// 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) {
self.init(storage: storage, cacheIdentifier: \.id)
}


/// 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.
/// - Parameter storage: A `StorageEngine` to initialize a ``Store`` instance with.
convenience init(storage: StorageEngine) async throws {
try await self.init(storage: storage, cacheIdentifier: \.id)
}
}

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 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.
///
/// ```
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
/// // Now the store will have `items` already loaded.
/// let items = await store.items
/// }
/// ```
///
/// - Alternatively you can use the synchronous initializer
/// and then await for items to load before accessing them.
/// 
/// ```
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
/// ```
///
/// This initializer eschews providing a `cacheIdentifier` when our `Item` conforms to `Identifiable`
/// with an `id` that is a `UUID`. 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) {
self.init(storage: storage, cacheIdentifier: \.id.uuidString)
}

/// 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 `UUID`. 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.uuidString)
}
}
69 changes: 60 additions & 9 deletions Sources/Boutique/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,35 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
/// 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 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.
///
/// ```
/// let store: Store<Item>
///
/// init() async throws {
/// store = try await Store(...)
/// // Now the store will have `items` already loaded.
/// let items = await store.items
/// }
/// ```
///
/// - Alternatively you can use the synchronous initializer
/// and then await for items to load before accessing them.
///
/// ```
/// let store: Store<Item> = Store(...)
///
/// func getItems() async -> [Item] {
/// try await store.itemsHaveLoaded()
/// return await store.items
/// }
/// ```
///
/// - Parameters:
/// - storage: A `StorageEngine` to initialize a ``Store`` instance with.
/// - cacheIdentifier: A `KeyPath` from the `Item` pointing to a `String`, which the ``Store``
Expand All @@ -66,15 +95,30 @@ public final class Store<Item: Codable & Equatable>: ObservableObject {
self.storageEngine = storage
self.cacheIdentifier = cacheIdentifier

Task { @MainActor in
do {
let decoder = JSONDecoder()
self.items = try await self.storageEngine.readAllData()
.map({ try decoder.decode(Item.self, from: $0) })
} catch {
self.items = []
}
}
// Begin loading items in the background.
_ = self.loadStoreTask
}

/// Initializes a new ``Store`` for persisting items to a memory cache
/// and a storage engine, to act as a source of truth, and await for the ``items`` to load.
///
/// - Parameters:
/// - storage: A `StorageEngine` to initialize a ``Store`` instance with.
/// - cacheIdentifier: A `KeyPath` from the `Item` pointing to a `String`, which the ``Store``
/// will use to create a unique identifier for the item when it's saved.
@MainActor
public init(storage: StorageEngine, cacheIdentifier: KeyPath<Item, String>) async throws {
self.storageEngine = storage
self.cacheIdentifier = cacheIdentifier
try await itemsHaveLoaded()
}

/// 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``.
public func itemsHaveLoaded() async throws {
try await loadStoreTask.value
}

/// Adds an item to the store.
Expand Down Expand Up @@ -256,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)
}


}
Loading

0 comments on commit ad47507

Please sign in to comment.