Skip to content

Instantly share code, notes, and snippets.

@PatrikTheDev
Last active April 29, 2021 09:28
Show Gist options
  • Save PatrikTheDev/5ab4153dce60be7b99b6017e4301ab19 to your computer and use it in GitHub Desktop.
Save PatrikTheDev/5ab4153dce60be7b99b6017e4301ab19 to your computer and use it in GitHub Desktop.
Observable Preferences (ugly implementation)
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