From d848d4f01e6be99db92de7bb30a74cd38ed8b82f Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Mon, 8 Apr 2024 20:01:35 -0400 Subject: [PATCH 1/3] Temporary workaround for a bug that prevents updating a SecurelyStoredValue --- Sources/Boutique/SecurelyStoredValue.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Boutique/SecurelyStoredValue.swift b/Sources/Boutique/SecurelyStoredValue.swift index 88940be..a3a8824 100644 --- a/Sources/Boutique/SecurelyStoredValue.swift +++ b/Sources/Boutique/SecurelyStoredValue.swift @@ -92,7 +92,12 @@ public struct SecurelyStoredValue { if self.wrappedValue == nil { try self.insert(value) } else { - try self.update(value) + // This call to `remove` is a temporary workaround for broken functionality when trying to update a value. + // Since updating a value does not seem to work, I've rewritten `set` to first set a `nil` value + // then the desired value, which will effectively call `set` with a new value, which does work. + // This will be fixed in the future, and we will restore the call-site to say `self.update(value)`. + try self.remove() + try self.insert(value) } } else { try self.remove() From 4b33c2860777706d4ef08449dd27227f75cf0e8f Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Wed, 10 Apr 2024 10:57:50 -0400 Subject: [PATCH 2/3] Removing BoutiquItem prefixes from test fixtures --- Tests/BoutiqueTests/AsyncStoreTests.swift | 156 +++++++++--------- .../BoutiqueTests/AsyncStoredValueTests.swift | 30 ++-- Tests/BoutiqueTests/BoutiqueItem.swift | 2 + .../SecurelyStoredValueTests.swift | 22 +-- Tests/BoutiqueTests/StoreTests.swift | 156 +++++++++--------- Tests/BoutiqueTests/StoredTests.swift | 156 +++++++++--------- Tests/BoutiqueTests/StoredValueTests.swift | 54 +++--- 7 files changed, 289 insertions(+), 287 deletions(-) diff --git a/Tests/BoutiqueTests/AsyncStoreTests.swift b/Tests/BoutiqueTests/AsyncStoreTests.swift index 7b9d5cc..95595e4 100644 --- a/Tests/BoutiqueTests/AsyncStoreTests.swift +++ b/Tests/BoutiqueTests/AsyncStoreTests.swift @@ -20,80 +20,80 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await asyncStore.insert(BoutiqueItem.coat) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + try await asyncStore.insert(.coat) + XCTAssertTrue(asyncStore.items.contains(.coat)) - try await asyncStore.insert(BoutiqueItem.belt) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + try await asyncStore.insert(.belt) + XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertEqual(asyncStore.items.count, 2) } @MainActor func testInsertingItems() async throws { - try await asyncStore.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + try await asyncStore.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(asyncStore.items.contains(.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(asyncStore.items.isEmpty) - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) XCTAssertEqual(asyncStore.items.count, 4) } @MainActor func testReadingItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) - XCTAssertEqual(asyncStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(asyncStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(asyncStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(asyncStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(asyncStore.items[0], .coat) + XCTAssertEqual(asyncStore.items[1], .sweater) + XCTAssertEqual(asyncStore.items[2], .purse) + XCTAssertEqual(asyncStore.items[3], .belt) XCTAssertEqual(asyncStore.items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) + try await asyncStore.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) XCTAssertEqual(newStore.items.count, 4) } @MainActor func testRemovingItems() async throws { - try await asyncStore.insert(BoutiqueItem.allItems) - try await asyncStore.remove(BoutiqueItem.coat) + try await asyncStore.insert(.allItems) + try await asyncStore.remove(.coat) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(asyncStore.items.contains(.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) - try await asyncStore.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.purse)) + try await asyncStore.remove([.sweater, .purse]) + XCTAssertFalse(asyncStore.items.contains(.sweater)) + XCTAssertFalse(asyncStore.items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await asyncStore.insert(BoutiqueItem.coat) + try await asyncStore.insert(.coat) XCTAssertEqual(asyncStore.items.count, 1) try await asyncStore.removeAll() - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore.removeAll() XCTAssertTrue(asyncStore.items.isEmpty) @@ -101,112 +101,112 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) try await asyncStore - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 3) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) try await asyncStore.removeAll() try await asyncStore - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertFalse(asyncStore.items.contains(.belt)) try await asyncStore - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + XCTAssertFalse(asyncStore.items.contains(.belt)) try await asyncStore.removeAll() try await asyncStore - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(asyncStore.items.count, 3) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await asyncStore - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(asyncStore.items.count, 2) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.coat)) - try await asyncStore.insert(BoutiqueItem.uniqueItems) + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(asyncStore.items.contains(.purse)) try await asyncStore .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.belt)) try await asyncStore .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(asyncStore.items.count, 1) - XCTAssertTrue(asyncStore.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(asyncStore.items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await asyncStore - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(asyncStore.items.count, 0) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(asyncStore.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -215,7 +215,7 @@ final class AsyncStoreTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems = [BoutiqueItem].uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") asyncStore.$items diff --git a/Tests/BoutiqueTests/AsyncStoredValueTests.swift b/Tests/BoutiqueTests/AsyncStoredValueTests.swift index af34745..56ac4d0 100644 --- a/Tests/BoutiqueTests/AsyncStoredValueTests.swift +++ b/Tests/BoutiqueTests/AsyncStoredValueTests.swift @@ -30,13 +30,13 @@ final class AsyncStoredValueTests: XCTestCase { } func testStorageEngineBackedStoredValue() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - try await self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + try await self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) try await self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) } func testBoolAsyncStoredValue() async throws { @@ -81,11 +81,11 @@ final class AsyncStoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArrayValue, []) - try await self.$storedArrayValue.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater]) + try await self.$storedArrayValue.append(.sweater) + XCTAssertEqual(self.storedArrayValue, [.sweater]) - try await self.$storedArrayValue.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater, BoutiqueItem.belt]) + try await self.$storedArrayValue.append(.belt) + XCTAssertEqual(self.storedArrayValue, [.sweater, .belt]) } func testStoredArrayValueTogglePresence() async throws { @@ -106,10 +106,10 @@ final class AsyncStoredValueTests: XCTestCase { } func testStoredBinding() async throws { - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.sweater).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.sweater).wrappedValue) - try await self.$storedBinding.set(BoutiqueItem.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + try await self.$storedBinding.set(.belt) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testStoredValuePublishedSubscription() async throws { @@ -122,15 +122,15 @@ final class AsyncStoredValueTests: XCTestCase { values.append(item) if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .sweater, .purse, .belt]) + XCTAssertEqual(values, [.coat, .sweater, .purse, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - try await self.$storedItem.set(BoutiqueItem.sweater) - try await self.$storedItem.set(BoutiqueItem.purse) - try await self.$storedItem.set(BoutiqueItem.belt) + try await self.$storedItem.set(.sweater) + try await self.$storedItem.set(.purse) + try await self.$storedItem.set(.belt) wait(for: [expectation], timeout: 1) } diff --git a/Tests/BoutiqueTests/BoutiqueItem.swift b/Tests/BoutiqueTests/BoutiqueItem.swift index 1fff748..4299030 100644 --- a/Tests/BoutiqueTests/BoutiqueItem.swift +++ b/Tests/BoutiqueTests/BoutiqueItem.swift @@ -34,7 +34,9 @@ extension BoutiqueItem { merchantID: "4", value: "Belt" ) +} +extension [BoutiqueItem] { static let allItems = [ BoutiqueItem.coat, BoutiqueItem.sweater, diff --git a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift index 8f53080..8ae0992 100644 --- a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift +++ b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift @@ -136,11 +136,11 @@ final class SecurelyStoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArray, nil) - try await self.$storedArray.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArray, [BoutiqueItem.sweater]) + try await self.$storedArray.append(.sweater) + XCTAssertEqual(self.storedArray, [.sweater]) - try await self.$storedArray.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArray, [BoutiqueItem.sweater, BoutiqueItem.belt]) + try await self.$storedArray.append(.belt) + XCTAssertEqual(self.storedArray, [.sweater, .belt]) } @MainActor @@ -150,8 +150,8 @@ final class SecurelyStoredValueTests: XCTestCase { // Using wrappedValue for our tests to work around the fact that Binding doesn't conform to Equatable XCTAssertEqual(self.$storedBinding.binding.wrappedValue, nil) - try self.$storedBinding.set(BoutiqueItem.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + try self.$storedBinding.set(.belt) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testPublishedValueSubscription() async throws { @@ -166,16 +166,16 @@ final class SecurelyStoredValueTests: XCTestCase { } if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .purse, .sweater, .belt]) + XCTAssertEqual(values, [.coat, .purse, .sweater, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - try await self.$storedItem.set(BoutiqueItem.coat) - try await self.$storedItem.set(BoutiqueItem.purse) - try await self.$storedItem.set(BoutiqueItem.sweater) - try await self.$storedItem.set(BoutiqueItem.belt) + try await self.$storedItem.set(.coat) + try await self.$storedItem.set(.purse) + try await self.$storedItem.set(.sweater) + try await self.$storedItem.set(.belt) await fulfillment(of: [expectation], timeout: 1) } diff --git a/Tests/BoutiqueTests/StoreTests.swift b/Tests/BoutiqueTests/StoreTests.swift index 6ca57d8..6a4069f 100644 --- a/Tests/BoutiqueTests/StoreTests.swift +++ b/Tests/BoutiqueTests/StoreTests.swift @@ -26,80 +26,80 @@ final class StoreTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await store.insert(BoutiqueItem.coat) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + try await store.insert(.coat) + XCTAssertTrue(store.items.contains(.coat)) - try await store.insert(BoutiqueItem.belt) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + try await store.insert(.belt) + XCTAssertTrue(store.items.contains(.belt)) XCTAssertEqual(store.items.count, 2) } @MainActor func testInsertingItems() async throws { - try await store.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + try await store.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(store.items.contains(.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(store.items.isEmpty) - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) XCTAssertEqual(store.items.count, 4) } @MainActor func testReadingItems() async throws { - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) - XCTAssertEqual(store.items[0], BoutiqueItem.coat) - XCTAssertEqual(store.items[1], BoutiqueItem.sweater) - XCTAssertEqual(store.items[2], BoutiqueItem.purse) - XCTAssertEqual(store.items[3], BoutiqueItem.belt) + XCTAssertEqual(store.items[0], .coat) + XCTAssertEqual(store.items[1], .sweater) + XCTAssertEqual(store.items[2], .purse) + XCTAssertEqual(store.items[3], .belt) XCTAssertEqual(store.items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await store.insert(BoutiqueItem.allItems) + try await store.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) XCTAssertEqual(newStore.items.count, 4) } @MainActor func testRemovingItems() async throws { - try await store.insert(BoutiqueItem.allItems) - try await store.remove(BoutiqueItem.coat) + try await store.insert(.allItems) + try await store.remove(.coat) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(store.items.contains(.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) - try await store.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(store.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(store.items.contains(BoutiqueItem.purse)) + try await store.remove([.sweater, .purse]) + XCTAssertFalse(store.items.contains(.sweater)) + XCTAssertFalse(store.items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await store.insert(BoutiqueItem.coat) + try await store.insert(.coat) XCTAssertEqual(store.items.count, 1) try await store.removeAll() - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) XCTAssertEqual(store.items.count, 4) try await store.removeAll() XCTAssertTrue(store.items.isEmpty) @@ -107,112 +107,112 @@ final class StoreTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) try await store - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 3) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) try await store.removeAll() try await store - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertFalse(store.items.contains(.belt)) try await store - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.coat)) + XCTAssertFalse(store.items.contains(.belt)) try await store.removeAll() try await store - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(store.items.count, 3) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertTrue(store.items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await store - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(store.items.count, 2) - XCTAssertTrue(store.items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(store.items.contains(BoutiqueItem.coat)) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.coat)) - try await store.insert(BoutiqueItem.uniqueItems) + try await store.insert(.uniqueItems) XCTAssertEqual(store.items.count, 4) try await store - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.purse)) + XCTAssertTrue(store.items.contains(.purse)) try await store .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.belt)) try await store .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(store.items.count, 1) - XCTAssertTrue(store.items.contains(BoutiqueItem.belt)) + XCTAssertTrue(store.items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await store - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(store.items.count, 0) - XCTAssertFalse(store.items.contains(BoutiqueItem.purse)) - XCTAssertFalse(store.items.contains(BoutiqueItem.belt)) - XCTAssertFalse(store.items.contains(BoutiqueItem.coat)) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -221,7 +221,7 @@ final class StoreTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems = [BoutiqueItem].uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") store.$items diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index 376cde7..a7019ca 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -23,44 +23,44 @@ final class StoredTests: XCTestCase { @MainActor func testInsertingItem() async throws { - try await $items.insert(BoutiqueItem.coat) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + try await $items.insert(.coat) + XCTAssertTrue(items.contains(.coat)) - try await $items.insert(BoutiqueItem.belt) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + try await $items.insert(.belt) + XCTAssertTrue(items.contains(.belt)) XCTAssertEqual(items.count, 2) } @MainActor func testInsertingItems() async throws { - try await $items.insert([BoutiqueItem.coat, BoutiqueItem.sweater, BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + try await $items.insert([.coat, .sweater, .sweater, .purse]) + XCTAssertTrue(items.contains(.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) } @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(items.isEmpty) - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) XCTAssertEqual(items.count, 4) } @MainActor func testReadingItems() async throws { - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) - XCTAssertEqual(items[0], BoutiqueItem.coat) - XCTAssertEqual(items[1], BoutiqueItem.sweater) - XCTAssertEqual(items[2], BoutiqueItem.purse) - XCTAssertEqual(items[3], BoutiqueItem.belt) + XCTAssertEqual(items[0], .coat) + XCTAssertEqual(items[1], .sweater) + XCTAssertEqual(items[2], .purse) + XCTAssertEqual(items[3], .belt) XCTAssertEqual(items.count, 4) } @MainActor func testReadingPersistedItems() async throws { - try await $items.insert(BoutiqueItem.allItems) + try await $items.insert(.allItems) // The new store has to fetch items from disk. let newStore = try await Store( @@ -70,34 +70,34 @@ final class StoredTests: XCTestCase { XCTAssertEqual(newStore.items.count, 4) - XCTAssertEqual(newStore.items[0], BoutiqueItem.coat) - XCTAssertEqual(newStore.items[1], BoutiqueItem.sweater) - XCTAssertEqual(newStore.items[2], BoutiqueItem.purse) - XCTAssertEqual(newStore.items[3], BoutiqueItem.belt) + XCTAssertEqual(newStore.items[0], .coat) + XCTAssertEqual(newStore.items[1], .sweater) + XCTAssertEqual(newStore.items[2], .purse) + XCTAssertEqual(newStore.items[3], .belt) } @MainActor func testRemovingItems() async throws { - try await $items.insert(BoutiqueItem.allItems) - try await $items.remove(BoutiqueItem.coat) + try await $items.insert(.allItems) + try await $items.remove(.coat) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertFalse(items.contains(.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) - try await $items.remove([BoutiqueItem.sweater, BoutiqueItem.purse]) - XCTAssertFalse(items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(items.contains(BoutiqueItem.purse)) + try await $items.remove([.sweater, .purse]) + XCTAssertFalse(items.contains(.sweater)) + XCTAssertFalse(items.contains(.purse)) } @MainActor func testRemoveAll() async throws { - try await $items.insert(BoutiqueItem.coat) + try await $items.insert(.coat) XCTAssertEqual(items.count, 1) try await $items.removeAll() - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) XCTAssertEqual(items.count, 4) try await $items.removeAll() XCTAssertTrue(items.isEmpty) @@ -105,112 +105,112 @@ final class StoredTests: XCTestCase { @MainActor func testChainingInsertOperations() async throws { - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) try await $items - .remove(BoutiqueItem.coat) - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.coat) + .insert(.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 3) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) try await $items.removeAll() try await $items - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .remove([BoutiqueItem.belt]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .remove([.belt]) + .insert(.sweater) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertFalse(items.contains(.belt)) try await $items - .insert(BoutiqueItem.belt) - .insert(BoutiqueItem.coat) - .insert(BoutiqueItem.purse) - .remove([BoutiqueItem.belt, .coat]) - .insert(BoutiqueItem.sweater) + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove([.belt, .coat]) + .insert(.sweater) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertFalse(items.contains(.coat)) + XCTAssertFalse(items.contains(.belt)) try await $items.removeAll() try await $items - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) .run() XCTAssertEqual(items.count, 3) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertTrue(items.contains(.coat)) } @MainActor func testChainingRemoveOperations() async throws { try await $items - .insert(BoutiqueItem.uniqueItems) - .remove(BoutiqueItem.belt) - .remove(BoutiqueItem.purse) + .insert(.uniqueItems) + .remove(.belt) + .remove(.purse) .run() XCTAssertEqual(items.count, 2) - XCTAssertTrue(items.contains(BoutiqueItem.sweater)) - XCTAssertTrue(items.contains(BoutiqueItem.coat)) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.coat)) - try await $items.insert(BoutiqueItem.uniqueItems) + try await $items.insert(.uniqueItems) XCTAssertEqual(items.count, 4) try await $items - .remove([BoutiqueItem.sweater, BoutiqueItem.coat]) - .remove(BoutiqueItem.belt) + .remove([.sweater, .coat]) + .remove(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.purse)) + XCTAssertTrue(items.contains(.purse)) try await $items .removeAll() - .insert(BoutiqueItem.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.belt)) try await $items .removeAll() - .remove(BoutiqueItem.belt) - .insert(BoutiqueItem.belt) + .remove(.belt) + .insert(.belt) .run() XCTAssertEqual(items.count, 1) - XCTAssertTrue(items.contains(BoutiqueItem.belt)) + XCTAssertTrue(items.contains(.belt)) } @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await $items - .insert(BoutiqueItem.coat) - .insert([BoutiqueItem.purse, BoutiqueItem.belt]) + .insert(.coat) + .insert([.purse, .belt]) XCTAssertEqual(items.count, 0) - XCTAssertFalse(items.contains(BoutiqueItem.purse)) - XCTAssertFalse(items.contains(BoutiqueItem.belt)) - XCTAssertFalse(items.contains(BoutiqueItem.coat)) + XCTAssertFalse(items.contains(.purse)) + XCTAssertFalse(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. @@ -219,7 +219,7 @@ final class StoredTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = BoutiqueItem.uniqueItems + let uniqueItems = [BoutiqueItem].uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") $items.$items diff --git a/Tests/BoutiqueTests/StoredValueTests.swift b/Tests/BoutiqueTests/StoredValueTests.swift index c69f753..3fc8360 100644 --- a/Tests/BoutiqueTests/StoredValueTests.swift +++ b/Tests/BoutiqueTests/StoredValueTests.swift @@ -35,43 +35,43 @@ final class StoredValueTests: XCTestCase { } func testStoredValueOperations() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - await self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + await self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) await self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - await self.$storedItem.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedItem, BoutiqueItem.sweater) + await self.$storedItem.set(.sweater) + XCTAssertEqual(self.storedItem, .sweater) } @MainActor func testStoredValueOnMainActorOperations() async throws { - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - self.$storedItem.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + self.$storedItem.set(.belt) + XCTAssertEqual(self.storedItem, .belt) self.$storedItem.reset() - XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + XCTAssertEqual(self.storedItem, .coat) - self.$storedItem.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedItem, BoutiqueItem.sweater) + self.$storedItem.set(.sweater) + XCTAssertEqual(self.storedItem, .sweater) } func testStoredNilValue() async throws { XCTAssertEqual(self.storedNilValue, nil) - await self.$storedNilValue.set(BoutiqueItem.belt) - XCTAssertEqual(self.storedNilValue, BoutiqueItem.belt) + await self.$storedNilValue.set(.belt) + XCTAssertEqual(self.storedNilValue, .belt) await self.$storedNilValue.reset() XCTAssertEqual(self.storedNilValue, nil) - await self.$storedNilValue.set(BoutiqueItem.sweater) - XCTAssertEqual(self.storedNilValue, BoutiqueItem.sweater) + await self.$storedNilValue.set(.sweater) + XCTAssertEqual(self.storedNilValue, .sweater) } func testStoredBoolValueToggle() async throws { @@ -103,11 +103,11 @@ final class StoredValueTests: XCTestCase { func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArrayValue, []) - await self.$storedArrayValue.append(BoutiqueItem.sweater) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater]) + await self.$storedArrayValue.append(.sweater) + XCTAssertEqual(self.storedArrayValue, [.sweater]) - await self.$storedArrayValue.append(BoutiqueItem.belt) - XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater, BoutiqueItem.belt]) + await self.$storedArrayValue.append(.belt) + XCTAssertEqual(self.storedArrayValue, [.sweater, .belt]) } func testStoredArrayValueTogglePresence() async throws { @@ -130,11 +130,11 @@ final class StoredValueTests: XCTestCase { @MainActor func testStoredBinding() async throws { // Using wrappedValue for our tests to work around the fact that Binding doesn't conform to Equatable - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.sweater).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.sweater).wrappedValue) - self.$storedBinding.set(BoutiqueItem.belt) + self.$storedBinding.set(.belt) - XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(BoutiqueItem.belt).wrappedValue) + XCTAssertEqual(self.$storedBinding.binding.wrappedValue, Binding.constant(.belt).wrappedValue) } func testPublishedValueSubscription() async throws { @@ -147,15 +147,15 @@ final class StoredValueTests: XCTestCase { values.append(item) if values.count == 4 { - XCTAssertEqual(values, [BoutiqueItem.coat, .purse, .sweater, .belt]) + XCTAssertEqual(values, [.coat, .purse, .sweater, .belt]) expectation.fulfill() } }) .store(in: &cancellables) - await self.$storedItem.set(BoutiqueItem.purse) - await self.$storedItem.set(BoutiqueItem.sweater) - await self.$storedItem.set(BoutiqueItem.belt) + await self.$storedItem.set(.purse) + await self.$storedItem.set(.sweater) + await self.$storedItem.set(.belt) await fulfillment(of: [expectation], timeout: 1) } From 221d9f8b0e5fc083ca080cffd73c0eeb8e772b3e Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Wed, 10 Apr 2024 15:36:04 -0400 Subject: [PATCH 3/3] Adding a layer of caching to skip JSON decoding when accessing a StoredValue --- Sources/Boutique/CachedValue.swift | 25 ++++++++++++++++++++++ Sources/Boutique/StoredValue+Binding.swift | 1 - Sources/Boutique/StoredValue.swift | 10 ++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 Sources/Boutique/CachedValue.swift diff --git a/Sources/Boutique/CachedValue.swift b/Sources/Boutique/CachedValue.swift new file mode 100644 index 0000000..4b443bb --- /dev/null +++ b/Sources/Boutique/CachedValue.swift @@ -0,0 +1,25 @@ +import Foundation + +/// `CachedValue` exists internally for the purpose of creating a reference value, preventing the need +/// to create a `JSONDecoder` and invoke a decode step every time we need to access a `StoredValue` externally. +internal final class CachedValue { + private var cachedValue: Item? + public var retrieveValue: () -> Item + + init(retrieveValue: @escaping () -> Item) { + self.retrieveValue = retrieveValue + self.cachedValue = self.retrieveValue() + } + + func set(_ value: Item) { + self.cachedValue = value + } + + var wrappedValue: Item? { + if let cachedValue { + cachedValue + } else { + self.retrieveValue() + } + } +} diff --git a/Sources/Boutique/StoredValue+Binding.swift b/Sources/Boutique/StoredValue+Binding.swift index cebf1b1..6383178 100644 --- a/Sources/Boutique/StoredValue+Binding.swift +++ b/Sources/Boutique/StoredValue+Binding.swift @@ -14,7 +14,6 @@ public extension StoredValue { } } - public extension SecurelyStoredValue { /// A convenient way to create a `Binding` from a `SecurelyStoredValue`. /// diff --git a/Sources/Boutique/StoredValue.swift b/Sources/Boutique/StoredValue.swift index 8833683..43ea42d 100644 --- a/Sources/Boutique/StoredValue.swift +++ b/Sources/Boutique/StoredValue.swift @@ -45,6 +45,8 @@ public struct StoredValue { private let userDefaults: UserDefaults private let itemSubject: CurrentValueSubject + private var cachedValue: CachedValue + public init(wrappedValue: Item, key: String, storage userDefaults: UserDefaults = UserDefaults.standard) { self.key = key self.defaultValue = wrappedValue @@ -52,11 +54,15 @@ public struct StoredValue { let initialValue = Self.storedValue(forKey: key, userDefaults: userDefaults, defaultValue: defaultValue) self.itemSubject = CurrentValueSubject(initialValue) + + self.cachedValue = CachedValue(retrieveValue: { + Self.storedValue(forKey: key, userDefaults: userDefaults, defaultValue: initialValue) + }) } /// The currently stored value public var wrappedValue: Item { - Self.storedValue(forKey: self.key, userDefaults: self.userDefaults, defaultValue: self.defaultValue) + self.cachedValue.retrieveValue() } /// A ``StoredValue`` which exposes ``set(_:)`` and ``reset()`` functions alongside a ``publisher``. @@ -94,6 +100,7 @@ public struct StoredValue { let boxedValue = BoxedValue(value: value) if let data = try? JSONCoders.encoder.encode(boxedValue) { self.userDefaults.set(data, forKey: self.key) + self.cachedValue.set(value) self.itemSubject.send(value) } } @@ -123,6 +130,7 @@ public struct StoredValue { let boxedValue = BoxedValue(value: self.defaultValue) if let data = try? JSONCoders.encoder.encode(boxedValue) { self.userDefaults.set(data, forKey: self.key) + self.cachedValue.set(self.defaultValue) self.itemSubject.send(self.defaultValue) } }