Skip to content

Commit

Permalink
Making StoredValue mutation functions operate on MainActor
Browse files Browse the repository at this point in the history
  • Loading branch information
mergesort committed Aug 27, 2023
1 parent 76f94c5 commit fa8b035
Show file tree
Hide file tree
Showing 7 changed files with 37 additions and 15 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions Sources/Boutique/StoredValue+Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public extension StoredValue {
/// ```
/// try await self.$redPandaList.append("Pabu")
/// ```
@MainActor
func append<Value>(_ value: Value) where Item == [Value] {
var updatedArray = self.wrappedValue
updatedArray.append(value)
Expand Down
1 change: 1 addition & 0 deletions Sources/Boutique/StoredValue+Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public extension StoredValue {
/// A convenient way to create a `Binding` from a `StoredValue`.
///
/// - Returns: A `Binding<Item>` of the `StoredValue<Item>` provided.
@MainActor
var binding: Binding<Item> {
Binding(get: {
self.wrappedValue
Expand Down
1 change: 1 addition & 0 deletions Sources/Boutique/StoredValue+Bool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public extension StoredValue where Item == Bool {
/// ```
/// self.appState.$proFeaturesEnabled.toggle()
/// ```
@MainActor
func toggle() {
self.set(!self.wrappedValue)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Boutique/StoredValue+Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public extension StoredValue {
/// ```
/// try await self.$redPandaList.update(key: "best", value: "Pabu")
/// ```
@MainActor
func update<Key: Hashable, Value>(key: Key, value: Value?) where Item == [Key: Value] {
var updatedDictionary = self.wrappedValue
updatedDictionary[key] = value
Expand Down
2 changes: 2 additions & 0 deletions Sources/Boutique/StoredValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public struct StoredValue<Item: Codable> {
/// 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) {
Expand Down Expand Up @@ -117,6 +118,7 @@ public struct StoredValue<Item: Codable> {
/// `@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) {
Expand Down
44 changes: 30 additions & 14 deletions Tests/BoutiqueTests/StoredValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)
}
Expand Down

0 comments on commit fa8b035

Please sign in to comment.