From c2d2815f6510c73bd1e09fc77b61357b69d20fe8 Mon Sep 17 00:00:00 2001 From: Pim Date: Thu, 18 Jan 2024 13:39:32 +0100 Subject: [PATCH] Updated StoredValue with specific types only --- .../Property Wrappers/StoredValue.swift | 196 +++++++++++++++--- .../DidUpdateTests/ViewModelStateTests.swift | 27 +++ 2 files changed, 191 insertions(+), 32 deletions(-) diff --git a/Sources/DidUpdate/Property Wrappers/StoredValue.swift b/Sources/DidUpdate/Property Wrappers/StoredValue.swift index 631a26c..60e5ac3 100644 --- a/Sources/DidUpdate/Property Wrappers/StoredValue.swift +++ b/Sources/DidUpdate/Property Wrappers/StoredValue.swift @@ -11,7 +11,7 @@ public struct StoredValue { let getter: () -> Value? let setter: (Value) -> Void - private var storage: Value { + var storage: Value { get { getter() ?? defaultValue } @@ -20,19 +20,19 @@ public struct StoredValue { } } - /// Creates a new StoredValue property wrapper - /// - Parameters: - /// - wrappedValue: Default value when value not found in `UserDefaults` - /// - key: Key to use to access `UserDefaults` - /// - store: `UserDefaults` store to use - public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) { - defaultValue = wrappedValue - getter = { - store.value(forKey: key) as? Value - } - setter = { value in - store.set(value, forKey: key) - } + @available( + *, unavailable, + message: "This property wrapper can only be applied to properties of classes conforming to ObservableState") + public var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } + + @available( + *, unavailable, + message: "This property wrapper can only be applied to properties of classes conforming to ObservableState") + public var projectedValue: ReadOnlyProxy { + fatalError() } /// Updates the enclosing ``ObservableState``’s ``StateObserver`` whenever the value is changed @@ -71,40 +71,172 @@ public struct StoredValue { ) } } +} - @available( - *, unavailable, - message: "This property wrapper can only be applied to properties of classes conforming to ObservableState") - public var wrappedValue: Value { - get { fatalError() } - set { fatalError() } +extension StoredValue { + /// Initializes StoredValue property wrapping using a default value + private init(defaultValue: Value, key: String, store: UserDefaults) { + self.defaultValue = defaultValue + getter = { + store.object(forKey: key) as? Value + } + setter = { value in + store.set(value, forKey: key) + } } - - @available( - *, unavailable, - message: "This property wrapper can only be applied to properties of classes conforming to ObservableState") - public var projectedValue: ReadOnlyProxy { - fatalError() + /// Creates a new StoredValue property wrapper for a Bool value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Bool { + self.init(defaultValue: wrappedValue, key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an Int value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Int { + self.init(defaultValue: wrappedValue, key: key, store: store) + } + /// Creates a new StoredValue property wrapper for a Double value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Double { + self.init(defaultValue: wrappedValue, key: key, store: store) + } + /// Creates a new StoredValue property wrapper for a String value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == String { + self.init(defaultValue: wrappedValue, key: key, store: store) + } + /// Creates a new StoredValue property wrapper for a URL value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == URL { + self.init(defaultValue: wrappedValue, key: key, store: store) + } + /// Creates a new StoredValue property wrapper for a Data value + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Data { + self.init(defaultValue: wrappedValue, key: key, store: store) } } -extension StoredValue where Value: ExpressibleByNilLiteral { - /// Creates a new StoredValue property wrapper with a default `nil` value +extension StoredValue { + /// Creates a new StoredValue property wrapper for a Set value, storing the value as an Array in the user defaults store /// - Parameters: /// - wrappedValue: Default value when value not found in `UserDefaults` /// - key: Key to use to access `UserDefaults` /// - store: `UserDefaults` store to use - public init(wrappedValue: Optional = nil, _ key: String, store: UserDefaults = .standard) where Value == Optional { - defaultValue = wrappedValue + public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Set { + self.init( + defaultValue: wrappedValue, + getter: { + guard let array = store.object(forKey: key) as? [Element] else { + return nil + } + return Set(array) + }, + setter: { value in + store.set(Array(value), forKey: key) + } + ) + } +} + +extension StoredValue where Value: ExpressibleByNilLiteral { + /// Initializes StoredValue property wrapping using a default optional value + private init(key: String, store: UserDefaults) where Value == Optional { + defaultValue = nil getter = { - store.value(forKey: key) as? Value + store.object(forKey: key) as? Value } setter = { value in if let value { - store.setValue(value, forKey: key) + store.set(value, forKey: key) } else { store.removeObject(forKey: key) } } } + /// Creates a new StoredValue property wrapper for an optional Bool value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == Bool? { + self.init(key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an optional Int value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == Int? { + self.init(key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an optional Double value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == Double? { + self.init(key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an optional String value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == String? { + self.init(key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an optional URL value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == URL? { + self.init(key: key, store: store) + } + /// Creates a new StoredValue property wrapper for an optional Data value + /// - Parameters: + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == Data? { + self.init(key: key, store: store) + } +} + +extension StoredValue { + /// Creates a new StoredValue property wrapper for an optional Set value, storing the value as an Array in the user defaults store + /// - Parameters: + /// - wrappedValue: Default value when value not found in `UserDefaults` + /// - key: Key to use to access `UserDefaults` + /// - store: `UserDefaults` store to use + public init(_ key: String, store: UserDefaults = .standard) where Value == Set? { + self.init( + defaultValue: nil, + getter: { + guard let array = store.object(forKey: key) as? [Element] else { + return nil + } + return Set(array) + }, + setter: { value in + if let value { + store.set(Array(value), forKey: key) + } else { + store.removeObject(forKey: key) + } + } + ) + } } diff --git a/Tests/DidUpdateTests/ViewModelStateTests.swift b/Tests/DidUpdateTests/ViewModelStateTests.swift index 464a8d7..0ef414e 100644 --- a/Tests/DidUpdateTests/ViewModelStateTests.swift +++ b/Tests/DidUpdateTests/ViewModelStateTests.swift @@ -42,6 +42,9 @@ final class ViewModelStateTests: XCTestCase { @ObservedValue var structProperty = ViewModelProperty() { didSet { structBoolean.value = true }} + + @StoredValue("StoredProperty") var storedProperty: Bool = false + @StoredValue("OptionalStoredProperty") var optionalStoredProperty: String? } class SomeView { @@ -254,4 +257,28 @@ final class ViewModelStateTests: XCTestCase { bool.expect(true, operation: { view.viewModel.optional = .zero }) bool.expect(false, operation: { view.viewModel.frame.size.height = 20 }) } + + func testStoredValues() { + let view = SomeView() + let bool = BooleanContainer() + + var observer = view.$viewModel.storedProperty.didChange { newValue in + bool.value = true + } + _ = observer + + let defaults = UserDefaults.standard + defaults.removeObject(forKey: "StoredProperty") + defaults.removeObject(forKey: "OptionalStoredProperty") + + bool.expect(false) { view.viewModel.storedProperty = false } + bool.expect(true) { view.viewModel.storedProperty = true } + + observer = view.$viewModel.optionalStoredProperty.didChange { newValue in + bool.value = true + } + + bool.expect(false) { view.viewModel.optionalStoredProperty = nil } + bool.expect(true) { view.viewModel.optionalStoredProperty = "someString" } + } }