From fa8b035845df9f406c9ee2d89709cc95f0ffd61e Mon Sep 17 00:00:00 2001 From: Joe Fabisevich Date: Sat, 26 Aug 2023 20:03:36 -0400 Subject: [PATCH] Making StoredValue mutation functions operate on MainActor --- README.md | 2 +- Sources/Boutique/StoredValue+Array.swift | 1 + Sources/Boutique/StoredValue+Binding.swift | 1 + Sources/Boutique/StoredValue+Bool.swift | 1 + Sources/Boutique/StoredValue+Dictionary.swift | 1 + Sources/Boutique/StoredValue.swift | 2 + Tests/BoutiqueTests/StoredValueTests.swift | 44 +++++++++++++------ 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b805398..189b988 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ $currentlySelectedTheme.set(.dark) $hasHapticsEnabled.toggle() ``` -The `@SecurelyStoredValue` can do everything a `@StoredValue` does, but instead of storing values in `UserDefaults` a `@SecurelyStoredValue` will persist items in the system's Keychain. This is perfect for storing sensitive values such as passwords or auth tokens, which you would not want to store in `UserDefaults`. +The `@SecurelyStoredValue` property wrapper can do everything a `@StoredValue` does, but instead of storing values in `UserDefaults` a `@SecurelyStoredValue` will persist items in the system's Keychain. This is perfect for storing sensitive values such as passwords or auth tokens, which you would not want to store in `UserDefaults`. You may not want to use `UserDefaults` or the system Keychain to store a value, in which case you can use your own `StorageEngine`. To do so you should use the `@AsyncStoredValue` property wrapper, which allows you to store a single value in a `StorageEngine` you provide. This isn't commonly needed, but it provides additional flexibility while staying true to Boutique's `@StoredValue` API. diff --git a/Sources/Boutique/StoredValue+Array.swift b/Sources/Boutique/StoredValue+Array.swift index e6aad4f..e726299 100644 --- a/Sources/Boutique/StoredValue+Array.swift +++ b/Sources/Boutique/StoredValue+Array.swift @@ -13,6 +13,7 @@ public extension StoredValue { /// ``` /// try await self.$redPandaList.append("Pabu") /// ``` + @MainActor func append(_ value: Value) where Item == [Value] { var updatedArray = self.wrappedValue updatedArray.append(value) diff --git a/Sources/Boutique/StoredValue+Binding.swift b/Sources/Boutique/StoredValue+Binding.swift index 702eefb..eccfc04 100644 --- a/Sources/Boutique/StoredValue+Binding.swift +++ b/Sources/Boutique/StoredValue+Binding.swift @@ -4,6 +4,7 @@ public extension StoredValue { /// A convenient way to create a `Binding` from a `StoredValue`. /// /// - Returns: A `Binding` of the `StoredValue` provided. + @MainActor var binding: Binding { Binding(get: { self.wrappedValue diff --git a/Sources/Boutique/StoredValue+Bool.swift b/Sources/Boutique/StoredValue+Bool.swift index 47bae3d..d140920 100644 --- a/Sources/Boutique/StoredValue+Bool.swift +++ b/Sources/Boutique/StoredValue+Bool.swift @@ -10,6 +10,7 @@ public extension StoredValue where Item == Bool { /// ``` /// self.appState.$proFeaturesEnabled.toggle() /// ``` + @MainActor func toggle() { self.set(!self.wrappedValue) } diff --git a/Sources/Boutique/StoredValue+Dictionary.swift b/Sources/Boutique/StoredValue+Dictionary.swift index 0fc8525..97e07b1 100644 --- a/Sources/Boutique/StoredValue+Dictionary.swift +++ b/Sources/Boutique/StoredValue+Dictionary.swift @@ -13,6 +13,7 @@ public extension StoredValue { /// ``` /// try await self.$redPandaList.update(key: "best", value: "Pabu") /// ``` + @MainActor func update(key: Key, value: Value?) where Item == [Key: Value] { var updatedDictionary = self.wrappedValue updatedDictionary[key] = value diff --git a/Sources/Boutique/StoredValue.swift b/Sources/Boutique/StoredValue.swift index f533c3d..8833683 100644 --- a/Sources/Boutique/StoredValue.swift +++ b/Sources/Boutique/StoredValue.swift @@ -89,6 +89,7 @@ public struct StoredValue { /// Within Boutique the @Stored property wrapper works very similarly. /// /// - Parameter value: The value to set @``StoredValue`` to. + @MainActor public func set(_ value: Item) { let boxedValue = BoxedValue(value: value) if let data = try? JSONCoders.encoder.encode(boxedValue) { @@ -117,6 +118,7 @@ public struct StoredValue { /// `@Published var items: [Item]` would let you use `items` as a regular `[Item]`, /// but $items projects `AnyPublisher<[Item], Never>` so you can subscribe to changes items produces. /// Within Boutique the @Stored property wrapper works very similarly. + @MainActor public func reset() { let boxedValue = BoxedValue(value: self.defaultValue) if let data = try? JSONCoders.encoder.encode(boxedValue) { diff --git a/Tests/BoutiqueTests/StoredValueTests.swift b/Tests/BoutiqueTests/StoredValueTests.swift index 13bd02a..cf5fee1 100644 --- a/Tests/BoutiqueTests/StoredValueTests.swift +++ b/Tests/BoutiqueTests/StoredValueTests.swift @@ -24,6 +24,7 @@ final class StoredValueTests: XCTestCase { @StoredValue(key: "storedBinding") private var storedBinding = BoutiqueItem.sweater + @MainActor override func setUp() { self.$storedItem.reset() self.$storedBoolValue.reset() @@ -36,6 +37,20 @@ final class StoredValueTests: XCTestCase { func testStoredValueOperations() async throws { XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + await self.$storedItem.set(BoutiqueItem.belt) + XCTAssertEqual(self.storedItem, BoutiqueItem.belt) + + await self.$storedItem.reset() + XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + + await self.$storedItem.set(BoutiqueItem.sweater) + XCTAssertEqual(self.storedItem, BoutiqueItem.sweater) + } + + @MainActor + func testStoredValueOnMainActorOperations() async throws { + XCTAssertEqual(self.storedItem, BoutiqueItem.coat) + self.$storedItem.set(BoutiqueItem.belt) XCTAssertEqual(self.storedItem, BoutiqueItem.belt) @@ -49,52 +64,53 @@ final class StoredValueTests: XCTestCase { func testStoredNilValue() async throws { XCTAssertEqual(self.storedNilValue, nil) - self.$storedNilValue.set(BoutiqueItem.belt) + await self.$storedNilValue.set(BoutiqueItem.belt) XCTAssertEqual(self.storedNilValue, BoutiqueItem.belt) - self.$storedNilValue.reset() + await self.$storedNilValue.reset() XCTAssertEqual(self.storedNilValue, nil) - self.$storedNilValue.set(BoutiqueItem.sweater) + await self.$storedNilValue.set(BoutiqueItem.sweater) XCTAssertEqual(self.storedNilValue, BoutiqueItem.sweater) } func testStoredBoolValueToggle() async throws { XCTAssertEqual(self.storedBoolValue, false) - self.$storedBoolValue.toggle() + await self.$storedBoolValue.toggle() XCTAssertEqual(self.storedBoolValue, true) - self.$storedBoolValue.set(false) + await self.$storedBoolValue.set(false) XCTAssertEqual(self.storedBoolValue, false) - self.$storedBoolValue.toggle() + await self.$storedBoolValue.toggle() XCTAssertEqual(self.storedBoolValue, true) } func testStoredDictionaryValueUpdate() async throws { XCTAssertEqual(self.storedDictionaryValue, [:]) - self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) + await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) - self.$storedDictionaryValue.update(key: BoutiqueItem.belt.merchantID, value: nil) + await self.$storedDictionaryValue.update(key: BoutiqueItem.belt.merchantID, value: nil) XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) - self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: nil) + await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: nil) XCTAssertEqual(self.storedDictionaryValue, [:]) } func testStoredArrayValueAppend() async throws { XCTAssertEqual(self.storedArrayValue, []) - self.$storedArrayValue.append(BoutiqueItem.sweater) + await self.$storedArrayValue.append(BoutiqueItem.sweater) XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater]) - self.$storedArrayValue.append(BoutiqueItem.belt) + await self.$storedArrayValue.append(BoutiqueItem.belt) XCTAssertEqual(self.storedArrayValue, [BoutiqueItem.sweater, BoutiqueItem.belt]) } + @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) @@ -120,9 +136,9 @@ final class StoredValueTests: XCTestCase { }) .store(in: &cancellables) - self.$storedItem.set(BoutiqueItem.purse) - self.$storedItem.set(BoutiqueItem.sweater) - self.$storedItem.set(BoutiqueItem.belt) + await self.$storedItem.set(BoutiqueItem.purse) + await self.$storedItem.set(BoutiqueItem.sweater) + await self.$storedItem.set(BoutiqueItem.belt) await fulfillment(of: [expectation], timeout: 1) }