Skip to content

Instantly share code, notes, and snippets.

@Peter-Schorn
Last active December 16, 2021 02:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Peter-Schorn/2b31fd03ccee3c78c2180c8f96a44888 to your computer and use it in GitHub Desktop.
Save Peter-Schorn/2b31fd03ccee3c78c2180c8f96a44888 to your computer and use it in GitHub Desktop.
SwiftUI AppStorage Reimplementation
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