-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add StoredState backed by UserDefaults (#18)
- Loading branch information
Showing
5 changed files
with
241 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import Foundation | ||
|
||
extension Application { | ||
/// The shared `UserDefaults` instance. | ||
public var userDefaults: Dependency<UserDefaults> { | ||
dependency(UserDefaults.standard) | ||
} | ||
|
||
/// `StoredState` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area. State is stored using `UserDefaults`. | ||
public struct StoredState<Value>: CustomStringConvertible { | ||
/// A private backing storage for the value. | ||
private var initial: () -> Value | ||
|
||
/// The current state value. | ||
public var value: Value { | ||
get { | ||
let userDefaults = Application.dependency(\.userDefaults) | ||
let cachedValue = shared.cache.get( | ||
scope.key, | ||
as: State<Value>.self | ||
) | ||
|
||
if let cachedValue = cachedValue { | ||
return cachedValue.value | ||
} | ||
|
||
guard | ||
let object = userDefaults.object(forKey: scope.key), | ||
let storedValue = object as? Value | ||
else { return initial() } | ||
|
||
return storedValue | ||
} | ||
set { | ||
let userDefaults = Application.dependency(\.userDefaults) | ||
|
||
shared.cache.set( | ||
value: Application.State( | ||
initial: newValue, | ||
scope: scope | ||
), | ||
forKey: scope.key | ||
) | ||
userDefaults.set(newValue, forKey: scope.key) | ||
} | ||
} | ||
|
||
/// The scope in which this state exists. | ||
let scope: Scope | ||
|
||
/** | ||
Creates a new state within a given scope initialized with the provided value. | ||
|
||
- Parameters: | ||
- value: The initial value of the state | ||
- scope: The scope in which the state exists | ||
*/ | ||
init( | ||
initial: @escaping @autoclosure () -> Value, | ||
scope: Scope | ||
) { | ||
self.initial = initial | ||
self.scope = scope | ||
} | ||
|
||
public var description: String { | ||
"StoredState<\(Value.self)>(\(value)) (\(scope.key))" | ||
} | ||
|
||
/// Removes the value from `UserDefaults`. | ||
public mutating func remove() { | ||
value = initial() | ||
Application.dependency(\.userDefaults).removeObject(forKey: scope.key) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import Foundation | ||
import Combine | ||
import SwiftUI | ||
|
||
/// `StoredState` is a property wrapper allowing SwiftUI views to subscribe to Application's state changes in a reactive way. State is stored using `UserDefaults`. Works similar to `State` and `Published`. | ||
@propertyWrapper public struct StoredState<Value>: DynamicProperty { | ||
/// Holds the singleton instance of `Application`. | ||
@ObservedObject private var app: Application = Application.shared | ||
|
||
/// Path for accessing `StoredState` from Application. | ||
private let keyPath: KeyPath<Application, Application.StoredState<Value>> | ||
|
||
/// Represents the current value of the `StoredState`. | ||
public var wrappedValue: Value { | ||
get { | ||
app.value(keyPath: keyPath).value | ||
} | ||
nonmutating set { | ||
var state = app.value(keyPath: keyPath) | ||
state.value = newValue | ||
} | ||
} | ||
|
||
/// A binding to the `State`'s value, which can be used with SwiftUI views. | ||
public var projectedValue: Binding<Value> { | ||
Binding( | ||
get: { wrappedValue }, | ||
set: { wrappedValue = $0 } | ||
) | ||
} | ||
|
||
/** | ||
Initializes the AppState with a `keyPath` for accessing `StoredState` in Application. | ||
|
||
- Parameter keyPath: The `KeyPath` for accessing `StoredState` in Application. | ||
*/ | ||
public init( | ||
_ keyPath: KeyPath<Application, Application.StoredState<Value>> | ||
) { | ||
self.keyPath = keyPath | ||
} | ||
|
||
/// A property wrapper's synthetic storage property. This is just for SwiftUI to mutate the `wrappedValue` and send event through `objectWillChange` publisher when the `wrappedValue` changes | ||
public static subscript<OuterSelf: ObservableObject>( | ||
_enclosingInstance observed: OuterSelf, | ||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>, | ||
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self> | ||
) -> Value { | ||
get { | ||
observed[keyPath: storageKeyPath].wrappedValue | ||
} | ||
set { | ||
guard | ||
let publisher = observed.objectWillChange as? ObservableObjectPublisher | ||
else { return } | ||
|
||
publisher.send() | ||
observed[keyPath: storageKeyPath].wrappedValue = newValue | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters