Last active
December 16, 2021 02:11
-
-
Save Peter-Schorn/2b31fd03ccee3c78c2180c8f96a44888 to your computer and use it in GitHub Desktop.
SwiftUI AppStorage Reimplementation
This file contains hidden or 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
import Foundation | |
import SwiftUI | |
import Combine | |
@propertyWrapper public struct CustomAppStorage<Value>: DynamicProperty { | |
private class Observer: NSObject, ObservableObject { | |
private let store: UserDefaults | |
private let key: String | |
private let defaultValue: Value | |
private var cancellables: Set<AnyCancellable> = [] | |
init( | |
store: UserDefaults, | |
key: String, | |
defaultValue: Value | |
) { | |
self.store = store | |
self.key = key | |
self.defaultValue = defaultValue | |
super.init() | |
self.store.addObserver( | |
self, | |
forKeyPath: self.key, | |
options: [.new], | |
context: nil | |
) | |
// self.objectWillChange | |
// .sink { | |
// print("objectWillChange") | |
// } | |
// .store(in: &self.cancellables) | |
} | |
override func observeValue( | |
forKeyPath keyPath: String?, | |
of object: Any?, | |
change: [NSKeyValueChangeKey : Any]?, | |
context: UnsafeMutableRawPointer? | |
) { | |
self.objectWillChange.send() | |
} | |
deinit { | |
self.store.removeObserver(self, forKeyPath: self.key) | |
} | |
} | |
@StateObject private var observer: Observer | |
private let store: UserDefaults | |
private let key: String | |
private let defaultValue: Value | |
/// If `nil` is returned, then `defaultValue` is used instead. | |
private let getFromUserDefaults: () -> Value? | |
private let saveToUserDefaults: (_ newValue: Value) -> Void | |
public var wrappedValue: Value { | |
get { | |
let value = self.getFromUserDefaults() ?? self.defaultValue | |
// print("wrappedValue:get: \(value)") | |
return value | |
} | |
nonmutating set { | |
// print("wrappedValue:set: \(newValue)") | |
self.observer.objectWillChange.send() | |
self.saveToUserDefaults(newValue) | |
} | |
} | |
public var projectedValue: Binding<Value> { | |
Binding( | |
get: { return self.wrappedValue }, | |
set: { newValue in self.wrappedValue = newValue } | |
) | |
} | |
} | |
// MARK: - Initializers - | |
extension CustomAppStorage { | |
/// Creates a property that can read and write to a boolean user default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a boolean value is not specified | |
/// for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Bool { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to an integer user default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if an integer value is not specified | |
/// for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Int { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to a double user default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a double value is not specified | |
/// for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Double { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to a string user default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a string value is not specified | |
/// for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == String { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.string(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to a url user default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a url value is not specified for | |
/// the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == URL { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.url(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to a user default as data. | |
/// | |
/// Avoid storing large data blobs in user defaults, such as image data, | |
/// as it can negatively affect performance of your app. On tvOS, a | |
/// `NSUserDefaultsSizeLimitExceededNotification` notification is posted | |
/// if the total user default size reaches 512kB. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a data value is not specified for | |
/// the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Data { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.data(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to an integer user default, | |
/// transforming that to `RawRepresentable` data type. | |
/// | |
/// A common usage is with enumerations: | |
/// | |
/// enum MyEnum: Int { | |
/// case a | |
/// case b | |
/// case c | |
/// } | |
/// struct MyView: View { | |
/// @AppStorage("MyEnumValue") private var value = MyEnum.a | |
/// var body: some View { ... } | |
/// } | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if an integer value | |
/// is not specified for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value : RawRepresentable, Value.RawValue == Int { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let rawValue = store.value(forKey: key) as? Int { | |
return Value(rawValue: rawValue) | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue.rawValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write to a string user default, | |
/// transforming that to `RawRepresentable` data type. | |
/// | |
/// A common usage is with enumerations: | |
/// | |
/// enum MyEnum: String { | |
/// case a | |
/// case b | |
/// case c | |
/// } | |
/// struct MyView: View { | |
/// @AppStorage("MyEnumValue") private var value = MyEnum.a | |
/// var body: some View { ... } | |
/// } | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a string value | |
/// is not specified for the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value : RawRepresentable, Value.RawValue == String { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let rawValue = store.string(forKey: key) { | |
return Value(rawValue: rawValue) | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue.rawValue, forKey: key) | |
} | |
} | |
} | |
extension CustomAppStorage where Value : ExpressibleByNilLiteral { | |
/// Creates a property that can read and write an Optional boolean user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Bool? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write an Optional integer user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Int? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write an Optional double user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Double? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.value(forKey: key) as? Value | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write an Optional string user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == String? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.string(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write an Optional URL user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == URL? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.url(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
/// Creates a property that can read and write an Optional data user | |
/// default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == Data? { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
store.data(forKey: key) | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue, forKey: key) | |
} | |
} | |
} | |
extension CustomAppStorage { | |
/// Creates a property that can save and restore an Optional string, | |
/// transforming it to an Optional `RawRepresentable` data type. | |
/// | |
/// Defaults to nil if there is no restored value | |
/// | |
/// A common usage is with enumerations: | |
/// | |
/// enum MyEnum: String { | |
/// case a | |
/// case b | |
/// case c | |
/// } | |
/// struct MyView: View { | |
/// @AppStorage("MyEnumValue") private var value: MyEnum? | |
/// var body: some View { ... } | |
/// } | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init<R>( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == R?, R : RawRepresentable, R.RawValue == String { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let rawValue = store.string(forKey: key) { | |
return R(rawValue: rawValue) | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue?.rawValue, forKey: key) | |
} | |
} | |
/// Creates a property that can save and restore an Optional integer, | |
/// transforming it to an Optional `RawRepresentable` data type. | |
/// | |
/// Defaults to nil if there is no restored value | |
/// | |
/// A common usage is with enumerations: | |
/// | |
/// enum MyEnum: Int { | |
/// case a | |
/// case b | |
/// case c | |
/// } | |
/// struct MyView: View { | |
/// @AppStorage("MyEnumValue") private var value: MyEnum? | |
/// var body: some View { ... } | |
/// } | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
public init<R>( | |
_ key: String, | |
store: UserDefaults? = nil | |
) where Value == R?, R : RawRepresentable, R.RawValue == Int { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let rawValue = store.value(forKey: key) as? R.RawValue { | |
return R(rawValue: rawValue) | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
store.set(newValue?.rawValue, forKey: key) | |
} | |
} | |
} | |
// MARK: - Custom Initializers - | |
extension CustomAppStorage { | |
/// Creates a property that can read and write a `Codable` type to a user | |
/// default. | |
/// | |
/// - Parameters: | |
/// - wrappedValue: The default value if a value does not yet exist for | |
/// the given key. | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
/// - decoder: The decoder to use to decode the value from data. | |
/// - encoder: The encoder to use to encode the value to data. | |
public init( | |
wrappedValue: Value, | |
_ key: String, | |
store: UserDefaults? = nil, | |
decoder: JSONDecoder = .init(), | |
encoder: JSONEncoder = .init() | |
) where Value: Codable { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = wrappedValue | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let data = store.data(forKey: key), | |
let value = try? decoder.decode(Value.self, from: data) { | |
return value | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
if let data = try? encoder.encode(newValue) { | |
store.set(data, forKey: key) | |
} | |
} | |
} | |
/// Creates a property that can read and write an Optional `Codable` type | |
/// to a user default. | |
/// | |
/// Defaults to nil if there is no restored value. | |
/// | |
/// - Parameters: | |
/// - key: The key to read and write the value to in the user defaults | |
/// store. | |
/// - store: The user defaults store to read and write to. A value | |
/// of `nil` will use the user default store from the environment. | |
/// - decoder: The decoder to use to decode the value from data. | |
/// - encoder: The encoder to use to encode the value to data. | |
public init<C>( | |
_ key: String, | |
store: UserDefaults? = nil, | |
decoder: JSONDecoder = .init(), | |
encoder: JSONEncoder = .init() | |
) where Value == C?, C: Codable { | |
let store = store ?? .standard | |
self.store = store | |
self.key = key | |
self.defaultValue = nil | |
let observer = Observer( | |
store: self.store, | |
key: self.key, | |
defaultValue: self.defaultValue | |
) | |
self._observer = StateObject(wrappedValue: observer) | |
self.getFromUserDefaults = { | |
if let data = store.data(forKey: key), | |
let value = try? decoder.decode(Value.self, from: data) { | |
return value | |
} | |
return nil | |
} | |
self.saveToUserDefaults = { newValue in | |
if let data = try? encoder.encode(newValue) { | |
store.set(data, forKey: key) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment