From 5c1d69fcca184bb7264a4cd2d95dafe39b750758 Mon Sep 17 00:00:00 2001 From: Amine Bensalah Date: Tue, 27 Dec 2022 19:25:40 +0100 Subject: [PATCH] feat: add propertywrapper to userdefaults --- .../CombineUserDefaultOnChange.swift | 8 +- .../UserDefaults/DefaultsObservation.swift | 13 ++- .../UserDefaults/DefaultsStorage.swift | 88 +++++++++++++++++++ .../UserDefaults/PropertyListValue.swift | 4 +- .../UserDefaults/UserDefaults+Extension.swift | 5 +- 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsStorage.swift diff --git a/Sources/CombineExtensionUserDefaults/UserDefaults/CombineUserDefaultOnChange.swift b/Sources/CombineExtensionUserDefaults/UserDefaults/CombineUserDefaultOnChange.swift index 9660843..411cbb7 100644 --- a/Sources/CombineExtensionUserDefaults/UserDefaults/CombineUserDefaultOnChange.swift +++ b/Sources/CombineExtensionUserDefaults/UserDefaults/CombineUserDefaultOnChange.swift @@ -15,9 +15,9 @@ public extension Publishers { public typealias Failure = Never private let userDefaults: UserDefaults - private let key: Key + private let key: UserDefaultsKey - public init(userDefaults: UserDefaults, key: Key) { + public init(userDefaults: UserDefaults, key: UserDefaultsKey) { self.userDefaults = userDefaults self.key = key } @@ -36,11 +36,11 @@ public extension Publishers { extension Publishers.DefaultsObservation { private final class SubscriptionDefaultsObservation: NSObject, Subscription where S.Input == T? { private var subscriber: S? - private var key: Key + private var key: UserDefaultsKey private var isDisposed: Bool = false private var userDefaults: UserDefaults - init(subscriber: S, userDefaults: UserDefaults, key: Key) { + init(subscriber: S, userDefaults: UserDefaults, key: UserDefaultsKey) { self.subscriber = subscriber self.userDefaults = userDefaults self.key = key diff --git a/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsObservation.swift b/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsObservation.swift index 4123003..5a011da 100644 --- a/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsObservation.swift +++ b/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsObservation.swift @@ -10,20 +10,17 @@ import Combine import CombineExtension final class DefaultsObservation: NSObject, Cancellable { - let key: Key - private var onChange: (T?) -> Void + let key: UserDefaultsKey var isDisposed: Bool = false var userDefaults: UserDefaults + private var onChange: (T?) -> Void - init(key: Key, userDefaults: UserDefaults = .standard, onChange: @escaping (T?) -> Void) { + init(key: UserDefaultsKey, userDefaults: UserDefaults = .standard, onChange: @escaping (T?) -> Void) { self.key = key self.onChange = onChange self.userDefaults = userDefaults - } - - func configure() -> DefaultsObservation { - userDefaults.addObserver(self, forKeyPath: key.rawValue, options: [.new], context: nil) - return self + super.init() + self.userDefaults.addObserver(self, forKeyPath: key.rawValue, options: [.new], context: nil) } // swiftlint:disable block_based_kvo diff --git a/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsStorage.swift b/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsStorage.swift new file mode 100644 index 0000000..d61daec --- /dev/null +++ b/Sources/CombineExtensionUserDefaults/UserDefaults/DefaultsStorage.swift @@ -0,0 +1,88 @@ +// +// File.swift +// +// +// Created by amine on 27/12/2022. +// + +import Combine +#if canImport(SwiftUI) +import SwiftUI + +/// 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 DefaultsStorage: DynamicProperty { + + @ObservedObject private var _value: DefaultsObservationStorage + + private init( + value: Value, + userDefaults: UserDefaults = .standard, + key: UserDefaultsKey + ) { + _value = DefaultsObservationStorage( + defaultValue: value, + key: key, + userDefaults: userDefaults + ) + } + + public var wrappedValue: Value { + get { + _value.value + } + nonmutating set { + _value.value = newValue + } + } + + public var projectedValue: Binding { + Binding( + get: { self.wrappedValue }, + set: { self.wrappedValue = $0 } + ) + } + + public var publisher: AnyPublisher { + _value.publisher + } +} + +@usableFromInline +final class DefaultsObservationStorage: NSObject, ObservableObject { + var defaultValue: T + let key: UserDefaultsKey + var isDisposed: Bool = false + var userDefaults: UserDefaults + var value: T { + get { userDefaults.value(forKey: key.rawValue) as? T ?? defaultValue } + set { userDefaults.setValue(newValue, forKey: key.rawValue) } + } + var publisher: AnyPublisher { subject.eraseToAnyPublisher() } + + private var subject: CurrentValueSubject + + init( + defaultValue: T, + key: UserDefaultsKey, + userDefaults: UserDefaults = .standard + ) { + self.defaultValue = defaultValue + self.key = key + self.userDefaults = userDefaults + self.subject = CurrentValueSubject(defaultValue) + super.init() + self.userDefaults.addObserver(self, forKeyPath: key.rawValue, options: [.new], context: nil) + } + + // swiftlint:disable block_based_kvo + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) { + guard let change = change, object != nil, keyPath == key.rawValue else { return } + let newValue = change[.newKey] as? T + subject.send(newValue ?? defaultValue) + } + // swiftlint:enable block_based_kvo +} + +#endif diff --git a/Sources/CombineExtensionUserDefaults/UserDefaults/PropertyListValue.swift b/Sources/CombineExtensionUserDefaults/UserDefaults/PropertyListValue.swift index a11976a..889c926 100644 --- a/Sources/CombineExtensionUserDefaults/UserDefaults/PropertyListValue.swift +++ b/Sources/CombineExtensionUserDefaults/UserDefaults/PropertyListValue.swift @@ -10,7 +10,7 @@ import CombineExtension public protocol PropertyListValue {} -public struct Key: RawRepresentable { +public struct UserDefaultsKey: RawRepresentable { public let rawValue: String public init(rawValue: String) { @@ -18,7 +18,7 @@ public struct Key: RawRepresentable { } } -extension Key: ExpressibleByStringLiteral { +extension UserDefaultsKey: ExpressibleByStringLiteral { public init(stringLiteral value: String) { rawValue = value } diff --git a/Sources/CombineExtensionUserDefaults/UserDefaults/UserDefaults+Extension.swift b/Sources/CombineExtensionUserDefaults/UserDefaults/UserDefaults+Extension.swift index c76aeb9..28f8227 100644 --- a/Sources/CombineExtensionUserDefaults/UserDefaults/UserDefaults+Extension.swift +++ b/Sources/CombineExtensionUserDefaults/UserDefaults/UserDefaults+Extension.swift @@ -10,14 +10,13 @@ import Combine import CombineExtension extension CombineExtension where Base: UserDefaults { - public func change(key: Key) -> AnyPublisher { + public func change(key: UserDefaultsKey) -> AnyPublisher { Publishers.DefaultsObservation(userDefaults: base, key: key) .eraseToAnyPublisher() } - public func onChange(key: Key, callback: @escaping (T?) -> Void) -> AnyCancellable { + public func onChange(key: UserDefaultsKey, callback: @escaping (T?) -> Void) -> AnyCancellable { DefaultsObservation(key: key, userDefaults: base, onChange: callback) - .configure() .eraseToAnyCancellable() } }