Created
May 27, 2022 15:35
-
-
Save aheze/a895c39052343e3d3703c977b6ba815d to your computer and use it in GitHub Desktop.
App Storage with `objectWillChange` support
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
/// based on https://github.com/xavierLowmiller/AppStorage | |
/// A property wrapper type that reflects a value from `UserDefaults` and | |
/// invalidates a view on a change in value in that user default. | |
@frozen @propertyWrapper public struct Saved<Value>: DynamicProperty { | |
@ObservedObject private var _value: Storage<Value> | |
let saveValue: (Value) -> Void | |
let key: String | |
var valueChanged: (() -> Void)? | |
private init(value: Value, store: UserDefaults, key: String, transform: @escaping (Any?) -> Value?, saveValue: @escaping (Value) -> Void) { | |
self._value = Storage(value: value, store: store, key: key, transform: transform) | |
self.saveValue = saveValue | |
self.key = key | |
} | |
public var wrappedValue: Value { | |
get { | |
_value.value | |
} | |
nonmutating set { | |
saveValue(newValue) | |
_value.value = newValue | |
if let valueChanged = valueChanged { | |
valueChanged() /// let the parent view model know the value changed | |
} else { | |
assertionFailure("`valueChanged` not set for \(self)") /// otherwise, crash the app. | |
} | |
} | |
} | |
public var projectedValue: Binding<Value> { | |
Binding( | |
get: { self.wrappedValue }, | |
set: { self.wrappedValue = $0 } | |
) | |
} | |
} | |
@usableFromInline | |
final class Storage<Value>: NSObject, ObservableObject { | |
@Published var value: Value | |
private let defaultValue: Value | |
private let store: UserDefaults | |
private let keyPath: String | |
private let transform: (Any?) -> Value? | |
init(value: Value, store: UserDefaults, key: String, transform: @escaping (Any?) -> Value?) { | |
self.value = value | |
self.defaultValue = value | |
self.store = store | |
self.keyPath = key | |
self.transform = transform | |
super.init() | |
store.addObserver(self, forKeyPath: key, options: [.new], context: nil) | |
} | |
deinit { | |
store.removeObserver(self, forKeyPath: keyPath) | |
} | |
override func observeValue( | |
forKeyPath keyPath: String?, | |
of object: Any?, | |
change: [NSKeyValueChangeKey: Any]?, | |
context: UnsafeMutableRawPointer? | |
) { | |
value = change?[.newKey].flatMap(transform) ?? defaultValue | |
} | |
} | |
public extension Saved where Value == Bool { | |
/// 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. | |
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) { | |
let store = (store ?? .standard) | |
let initialValue = store.value(forKey: key) as? Value ?? wrappedValue | |
self.init(value: initialValue, store: store, key: key, transform: { | |
$0 as? Value | |
}, saveValue: { newValue in | |
store.setValue(newValue, forKey: key) | |
}) | |
} | |
} | |
public extension Saved where Value == Int { | |
/// 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. | |
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) { | |
let store = (store ?? .standard) | |
let initialValue = store.value(forKey: key) as? Value ?? wrappedValue | |
self.init(value: initialValue, store: store, key: key, transform: { | |
$0 as? Value | |
}, saveValue: { newValue in | |
store.setValue(newValue, forKey: key) | |
}) | |
} | |
} |
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
class StorageModel: ObservableObject { | |
static let data = StorageModelData.self | |
@Saved(data.defaultTab.key) var defaultTab = data.defaultTab.value | |
@Saved(data.swipeToNavigate.key) var swipeToNavigate = data.swipeToNavigate.value | |
init() { | |
/// start listening to the `valueChanged` closure | |
_defaultTab.configureValueChanged(with: self) | |
_swipeToNavigate.configureValueChanged(with: self) | |
} | |
} | |
/// contains all the default keys and values | |
enum StorageModelData { | |
static var defaultTab = SavedData(key: "defaultTab", value: 2) | |
static var swipeToNavigate = SavedData(key: "swipeToNavigate", value: true) | |
} | |
extension Saved { | |
/// listen to value changed | |
mutating func configureValueChanged(with model: StorageModel) { | |
let key = self.key | |
valueChanged = { [weak model] in | |
model?.objectWillChange.send() | |
NotificationCenter.default.post(name: Notification.Name(key), object: nil) | |
} | |
} | |
} | |
extension NSObject { | |
/// listen to a Storage Model Defaults notification, calling the selector at first too | |
/// Combine's `sink` does not work on the custom property wrapper, so I had to use notification center as a workaround. | |
func listen(to key: String, selector: Selector) { | |
NotificationCenter.default.addObserver(self, selector: selector, name: NSNotification.Name(key), object: nil) | |
perform(selector) | |
} | |
} |
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
extension ViewController { | |
func listenToDefaults() { | |
/// listens to the notification center (Sombine's sink doesn't work on `Saved`) | |
self.listen(to: StorageModelData.swipeToNavigate.key, selector: #selector(self.swipeToNavigateChanged)) | |
} | |
@objc func swipeToNavigateChanged() { | |
self.contentCollectionView.isScrollEnabled = self.getScrollViewEnabled() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment