Last active
April 29, 2021 09:28
-
-
Save PatrikTheDev/5ab4153dce60be7b99b6017e4301ab19 to your computer and use it in GitHub Desktop.
Observable Preferences (ugly implementation)
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
public class Preferences: ObservableObject { // It actually doesn't matter that it's an ObservableObject but there's no reason for it not to be | |
@UserDefault("workout-length") public var workoutLength = Defaults.workoutLength | |
@UserDefault("rest-length") public var restLength = Defaults.restLengh | |
@UserDefault("workout-phase-set-identifiers") public var workoutPhaseSetIds = [String]() | |
#if !os(watchOS) | |
@UserDefault("playlist-id") public var playlistID: String? | |
@CodableUserDefault("workout-communicator") public var workoutCommunicator: WorkoutCommunicators? // This is an example of a codable enum getting saved into UserDefaults | |
#endif | |
// Default values | |
public enum Defaults { | |
public static var workoutLength: TimeInterval = 20 | |
public static var restLengh: TimeInterval = 10 | |
} | |
public static var shared = Preferences() | |
} | |
public typealias Defaults = Preferences.Defaults | |
@propertyWrapper | |
public class UserDefault<Value: Equatable>: Resettable, ObservableObject { | |
var didChangeNotification = Notification.Name(rawValue: "UserDefaultsDidChange") | |
private var cancellables = Set<AnyCancellable>() | |
@Published public var defaultValue: Value | |
private(set) public var userDefaults: UserDefaults | |
public var key: String? | |
private var cachedValue: Value? | |
public var wrappedValue: Value { | |
get { | |
if let cached = cachedValue { | |
return cached | |
} else if let value = getValue() { | |
self.cachedValue = value | |
return value | |
} else { | |
return defaultValue | |
} | |
} | |
set { | |
cachedValue = newValue | |
setValue(newValue) | |
publisher.send() | |
isUpdating = false | |
notificationId = UUID().uuidString | |
NotificationCenter.default.post( | |
name: didChangeNotification, | |
object: userDefaults, | |
userInfo: ["callbackID": notificationId!] | |
) | |
} | |
} | |
lazy var publisher = objectWillChange | |
lazy var binding = Binding { | |
self.wrappedValue | |
} set: { | |
self.wrappedValue = $0 | |
} | |
public var projectedValue: UserDefault<Value> { self } | |
public init(wrappedValue: Value, _ key: String? = nil, defaults: UserDefaults = .standard) { | |
self.defaultValue = wrappedValue | |
self.userDefaults = defaults | |
self.key = key | |
NotificationCenter.default.publisher(for: didChangeNotification) | |
.sink { [weak self] in | |
guard let self = self else { return } | |
guard let id = $0.userInfo?["callbackID"] as? String, | |
id != self.notificationId, | |
($0.object as! UserDefaults) != self.userDefaults, | |
self.isUpdating else { | |
self.isUpdating = true | |
self.notificationId = nil | |
return | |
} | |
let newValue = self.getValue() // Gets the uncached version from UserDefaults | |
guard self.cachedValue != newValue else { return } | |
self.cachedValue = newValue | |
} | |
.store(in: &cancellables) | |
} | |
// In place to conform to a protocol I have, it allows to reset all properties with a Mirror | |
// Coincidentally that's also why the properties of the Preferences object aren't static, you can't enumerate those (not that I know of, it may be possible with something like Sourcery) | |
public func reset() { | |
guard let key = key else { return } | |
userDefaults.removeObject(forKey: key) | |
} | |
// These two methods are there so that I can replace the implementation in subclasses (see CodableUserDefault) | |
public func setValue(_ value: Value) { | |
guard let key = key else { return } | |
userDefaults.set(value, forKey: key) | |
} | |
public func getValue() -> Value? { | |
guard let key = key else { return nil } | |
return userDefaults.value(forKey: key) as? Value | |
} | |
private var isUpdating = true | |
private var notificationId: String? | |
} | |
public extension UserDefault where Value: ExpressibleByNilLiteral { | |
@inlinable | |
convenience init(_ key: String, defaults: UserDefaults = .standard) { | |
self.init(wrappedValue: nil, key, defaults: defaults) | |
} | |
} | |
@propertyWrapper | |
public class CodableUserDefault<Value>: UserDefault<Value> where Value: Codable, Value: Equatable { | |
public override func setValue(_ value: Value) { | |
guard let key = key else { return } | |
let data = try! JSONEncoder().encode(value) | |
userDefaults.set(data, forKey: key) | |
} | |
public override func getValue() -> Value? { | |
guard let key = key, let data = userDefaults.data(forKey: key) else { return nil } | |
return try? JSONDecoder().decode(Value.self, from: data) | |
} | |
public override var wrappedValue: Value { | |
get { super.wrappedValue } | |
set { super.wrappedValue = newValue } | |
} | |
public override var projectedValue: CodableUserDefault<Value> { self } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment