Created
September 10, 2018 15:22
-
-
Save krimpedance/27a2e3c5a7d301b4b468cca944279357 to your computer and use it in GitHub Desktop.
UserDefaults * Swifty * Observable * Testable
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
import Foundation | |
// MARK: - Observable ---------------- | |
public typealias UDChangeHandler<O, V> = (O, UDKeyValueObservedChange<V>) -> Void | |
public enum UDKeyValueChange: UInt { | |
case setting | |
case insertion | |
case removal | |
case replacement | |
} | |
public struct UDKeyValueObservedChange<Value> { | |
public typealias Kind = UDKeyValueChange | |
public let kind: Kind | |
public let newValue: Value? | |
public let oldValue: Value? | |
public let indexes: IndexSet? | |
public let isPrior: Bool | |
} | |
public protocol UDKeyValueCodingAndObserving {} | |
public protocol KeyValueObservation { | |
func invalidate() | |
} | |
public class UDKeyValueObservation: NSObject, KeyValueObservation { | |
private var previousTime: UInt64 = 0 | |
@nonobjc weak var object: NSObject? | |
@nonobjc let callback: (NSObject, UDKeyValueObservedChange<Any>) -> Void | |
@nonobjc let defaultName: String | |
fileprivate init(object: NSObject, defaultName: String, callback: @escaping (NSObject, UDKeyValueObservedChange<Any>) -> Void) { | |
self.object = object | |
self.defaultName = defaultName | |
self.callback = callback | |
} | |
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { | |
guard | |
let ourObject = self.object, | |
let change = change, | |
object as? NSObject == ourObject, | |
keyPath == defaultName | |
else { return } | |
let newTime = mach_absolute_time() | |
if object is UserDefaults && newTime <= (previousTime + 5_000_000) { return } | |
previousTime = newTime | |
let rawKind: UInt = change[.kindKey] as! UInt | |
let kind = UDKeyValueChange(rawValue: rawKind)! | |
let notification = UDKeyValueObservedChange(kind: kind, | |
newValue: change[.newKey], | |
oldValue: change[.oldKey], | |
indexes: change[.indexesKey] as! IndexSet?, | |
isPrior: change[.notificationIsPriorKey] as? Bool ?? false) | |
callback(ourObject, notification) | |
} | |
fileprivate func start(_ options: NSKeyValueObservingOptions) { | |
object?.addObserver(self, forKeyPath: defaultName, options: options, context: nil) | |
} | |
public func invalidate() { | |
object?.removeObserver(self, forKeyPath: defaultName, context: nil) | |
object = nil | |
} | |
deinit { | |
object?.removeObserver(self, forKeyPath: defaultName, context: nil) | |
} | |
} | |
public extension UDKeyValueCodingAndObserving { | |
func observe<Value>(_ type: Value.Type, forKey defaultName: String, options: NSKeyValueObservingOptions = [], changeHandler: @escaping UDChangeHandler<Self, Value>) -> UDKeyValueObservation { | |
let result = UDKeyValueObservation(object: self as! NSObject, defaultName: defaultName) { obj, change in | |
let notification = UDKeyValueObservedChange(kind: change.kind, | |
newValue: change.newValue as? Value, | |
oldValue: change.oldValue as? Value, | |
indexes: change.indexes, | |
isPrior: change.isPrior) | |
changeHandler(obj as! Self, notification) | |
} | |
result.start(options) | |
return result | |
} | |
func observe<Value>(_ type: Value.Type, forKey defaultName: String, options: NSKeyValueObservingOptions = [], changeHandler: @escaping UDChangeHandler<Self, Value>) -> UDKeyValueObservation where Value: Equatable { | |
let result = UDKeyValueObservation(object: self as! NSObject, defaultName: defaultName) { obj, change in | |
let newValue = change.newValue as? Value | |
let oldValue = change.oldValue as? Value | |
if newValue == oldValue { return } | |
let notification = UDKeyValueObservedChange(kind: change.kind, | |
newValue: newValue, | |
oldValue: oldValue, | |
indexes: change.indexes, | |
isPrior: change.isPrior) | |
changeHandler(obj as! Self, notification) | |
} | |
result.start(options) | |
return result | |
} | |
} | |
extension NSKeyValueObservation: KeyValueObservation {} | |
extension UserDefaults: UDKeyValueCodingAndObserving {} | |
// MARK: - Testable ---------------- | |
public protocol UserDefaultable: UDKeyValueCodingAndObserving { | |
func object(forKey defaultName: String) -> Any? | |
func string(forKey defaultName: String) -> String? | |
func dictionary(forKey defaultName: String) -> [String: Any]? | |
func data(forKey defaultName: String) -> Data? | |
func stringArray(forKey defaultName: String) -> [String]? | |
func integer(forKey defaultName: String) -> Int | |
func float(forKey defaultName: String) -> Float | |
func double(forKey defaultName: String) -> Double | |
func bool(forKey defaultName: String) -> Bool | |
func url(forKey defaultName: String) -> URL? | |
func set(_ value: Any?, forKey defaultName: String) | |
func set(_ value: Int, forKey defaultName: String) | |
func set(_ value: Float, forKey defaultName: String) | |
func set(_ value: Double, forKey defaultName: String) | |
func set(_ value: Bool, forKey defaultName: String) | |
func set(_ url: URL?, forKey defaultName: String) | |
func removeObject(forKey defaultName: String) | |
func reset() | |
} | |
extension UserDefaults: UserDefaultable { | |
public func reset() { | |
guard let bundleId = Bundle.main.bundleIdentifier else { return } | |
UserDefaults.standard.removePersistentDomain(forName: bundleId) | |
} | |
} | |
public class UserDefaultsFake: NSObject, UserDefaultable { | |
private var values = [String: Any]() | |
public init(values: [String: Any] = [:]) { | |
self.values = values | |
} | |
override public func value(forKey key: String) -> Any? { | |
return values[key] | |
} | |
} | |
public extension UserDefaultsFake { | |
func object(forKey defaultName: String) -> Any? { | |
return values[defaultName] | |
} | |
func string(forKey defaultName: String) -> String? { | |
return values[defaultName] as? String | |
} | |
func dictionary(forKey defaultName: String) -> [String: Any]? { | |
return values[defaultName] as? [String: Any] | |
} | |
func data(forKey defaultName: String) -> Data? { | |
return values[defaultName] as? Data | |
} | |
func stringArray(forKey defaultName: String) -> [String]? { | |
return values[defaultName] as? [String] | |
} | |
func integer(forKey defaultName: String) -> Int { | |
return values[defaultName] as? Int ?? 0 | |
} | |
func float(forKey defaultName: String) -> Float { | |
return values[defaultName] as? Float ?? 0 | |
} | |
func double(forKey defaultName: String) -> Double { | |
return values[defaultName] as? Double ?? 0 | |
} | |
func bool(forKey defaultName: String) -> Bool { | |
return values[defaultName] as? Bool ?? false | |
} | |
func url(forKey defaultName: String) -> URL? { | |
return values[defaultName] as? URL | |
} | |
func set(_ value: Any?, forKey defaultName: String) { | |
guard let val = value else { removeObject(forKey: defaultName); return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = val | |
didChangeValue(forKey: defaultName) | |
} | |
func set(_ value: Int, forKey defaultName: String) { | |
if let current = values[defaultName] as? Int, current == value { return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = value | |
didChangeValue(forKey: defaultName) | |
} | |
func set(_ value: Float, forKey defaultName: String) { | |
if let current = values[defaultName] as? Float, current == value { return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = value | |
didChangeValue(forKey: defaultName) | |
} | |
func set(_ value: Double, forKey defaultName: String) { | |
if let current = values[defaultName] as? Double, current == value { return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = value | |
didChangeValue(forKey: defaultName) | |
} | |
func set(_ value: Bool, forKey defaultName: String) { | |
if let current = values[defaultName] as? Bool, current == value { return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = value | |
didChangeValue(forKey: defaultName) | |
} | |
func set(_ url: URL?, forKey defaultName: String) { | |
guard let val = url else { removeObject(forKey: defaultName); return } | |
if let current = values[defaultName] as? URL, current == val { return } | |
willChangeValue(forKey: defaultName) | |
values[defaultName] = val | |
didChangeValue(forKey: defaultName) | |
} | |
func removeObject(forKey defaultName: String) { | |
if values[defaultName] == nil { return } | |
willChangeValue(forKey: defaultName) | |
values.removeValue(forKey: defaultName) | |
didChangeValue(forKey: defaultName) | |
} | |
func reset() { | |
let allKeys = values.keys.map { $0 } | |
allKeys.forEach { willChangeValue(forKey: $0) } | |
values = [:] | |
allKeys.reversed().forEach { didChangeValue(forKey: $0) } | |
} | |
} | |
// MARK: - Swifty ------------------------ | |
public protocol KeyNamespaceable {} | |
extension KeyNamespaceable { | |
static func namespace<T: RawRepresentable>(_ key: T) -> String where T.RawValue == String { | |
return "\(Self.self)_\(T.self)_\(key.rawValue)" | |
} | |
} | |
public protocol ObjectUserDefaultable: KeyNamespaceable { associatedtype ObjectDefaultKey: RawRepresentable } | |
public protocol StringUserDefaultable: KeyNamespaceable { associatedtype StringDefaultKey: RawRepresentable } | |
public protocol DictionaryUserDefaultable: KeyNamespaceable { associatedtype DictionaryDefaultKey: RawRepresentable } | |
public protocol DataUserDefaultable: KeyNamespaceable { associatedtype DataDefaultKey: RawRepresentable } | |
public protocol StringArrayUserDefaultable: KeyNamespaceable { associatedtype StringArrayDefaultKey: RawRepresentable } | |
public protocol IntUserDefaultable: KeyNamespaceable { associatedtype IntDefaultKey: RawRepresentable } | |
public protocol FloatUserDefaultable: KeyNamespaceable { associatedtype FloatDefaultKey: RawRepresentable } | |
public protocol DoubleUserDefaultable: KeyNamespaceable { associatedtype DoubleDefaultKey: RawRepresentable } | |
public protocol BoolUserDefaultable: KeyNamespaceable { associatedtype BoolDefaultKey: RawRepresentable } | |
public protocol URLUserDefaultable: KeyNamespaceable { associatedtype URLDefaultKey: RawRepresentable } | |
public extension ObjectUserDefaultable where ObjectDefaultKey.RawValue == String { | |
static func set(_ value: Any?, forKey key: ObjectDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func object(for key: ObjectDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Any? { | |
return source.object(forKey: namespace(key)) | |
} | |
static func remove(for key: ObjectDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: ObjectDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Any>) -> UDKeyValueObservation { | |
return source.observe(Any.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
static func observe<Value>(_ type: Value.Type, | |
forKey key: ObjectDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Value>) -> UDKeyValueObservation { | |
return source.observe(type, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension StringUserDefaultable where StringDefaultKey.RawValue == String { | |
static func set(_ value: String?, forKey key: StringDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func string(for key: StringDefaultKey, source: UserDefaultable = UserDefaults.standard) -> String? { | |
return source.string(forKey: namespace(key)) | |
} | |
static func remove(for key: StringDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: StringDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, String>) -> UDKeyValueObservation { | |
return source.observe(String.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension DictionaryUserDefaultable where DictionaryDefaultKey.RawValue == String { | |
static func set(_ value: [String: Any]?, forKey key: DictionaryDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func dictionary(for key: DictionaryDefaultKey, source: UserDefaultable = UserDefaults.standard) -> [String: Any]? { | |
return source.dictionary(forKey: namespace(key)) | |
} | |
static func remove(for key: DictionaryDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: DictionaryDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, [String: Any]>) -> UDKeyValueObservation { | |
return source.observe([String: Any].self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension DataUserDefaultable where DataDefaultKey.RawValue == String { | |
static func set(_ value: Data?, forKey key: DataDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func data(for key: DataDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Data? { | |
return source.data(forKey: namespace(key)) | |
} | |
static func remove(for key: DataDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: DataDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Data>) -> UDKeyValueObservation { | |
return source.observe(Data.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension StringArrayUserDefaultable where StringArrayDefaultKey.RawValue == String { | |
static func set(_ value: [String]?, forKey key: StringArrayDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func stringArray(for key: StringArrayDefaultKey, source: UserDefaultable = UserDefaults.standard) -> [String]? { | |
return source.stringArray(forKey: namespace(key)) | |
} | |
static func remove(for key: StringArrayDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: StringArrayDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, [String]>) -> UDKeyValueObservation { | |
return source.observe([String].self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension IntUserDefaultable where IntDefaultKey.RawValue == String { | |
static func set(_ value: Int?, forKey key: IntDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
guard let val = value else { remove(for: key); return } | |
source.set(val, forKey: namespace(key)) | |
} | |
static func integer(for key: IntDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Int { | |
return source.integer(forKey: namespace(key)) | |
} | |
static func remove(for key: IntDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: IntDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Int>) -> UDKeyValueObservation { | |
return source.observe(Int.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension FloatUserDefaultable where FloatDefaultKey.RawValue == String { | |
static func set(_ value: Float?, forKey key: FloatDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
guard let val = value else { remove(for: key); return } | |
source.set(val, forKey: namespace(key)) | |
} | |
static func float(for key: FloatDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Float { | |
return source.float(forKey: namespace(key)) | |
} | |
static func remove(for key: FloatDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: FloatDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Float>) -> UDKeyValueObservation { | |
return source.observe(Float.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension DoubleUserDefaultable where DoubleDefaultKey.RawValue == String { | |
static func set(_ value: Double?, forKey key: DoubleDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
guard let val = value else { remove(for: key); return } | |
source.set(val, forKey: namespace(key)) | |
} | |
static func double(for key: DoubleDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Double { | |
return source.double(forKey: namespace(key)) | |
} | |
static func remove(for key: DoubleDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: DoubleDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Double>) -> UDKeyValueObservation { | |
return source.observe(Double.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension BoolUserDefaultable where BoolDefaultKey.RawValue == String { | |
static func set(_ value: Bool?, forKey key: BoolDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
guard let val = value else { remove(for: key); return } | |
source.set(val, forKey: namespace(key)) | |
} | |
static func bool(for key: BoolDefaultKey, source: UserDefaultable = UserDefaults.standard) -> Bool { | |
return source.bool(forKey: namespace(key)) | |
} | |
static func remove(for key: BoolDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: BoolDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, Bool>) -> UDKeyValueObservation { | |
return source.observe(Bool.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
public extension URLUserDefaultable where URLDefaultKey.RawValue == String { | |
static func defaultName(of key: URLDefaultKey) -> String { return namespace(key) } | |
static func set(_ value: URL?, forKey key: URLDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.set(value, forKey: namespace(key)) | |
} | |
static func url(for key: URLDefaultKey, source: UserDefaultable = UserDefaults.standard) -> URL? { | |
return source.url(forKey: namespace(key)) | |
} | |
static func remove(for key: URLDefaultKey, source: UserDefaultable = UserDefaults.standard) { | |
source.removeObject(forKey: namespace(key)) | |
} | |
static func observe(for key: URLDefaultKey, | |
source: UserDefaultable = UserDefaults.standard, | |
options: NSKeyValueObservingOptions = [], | |
changeHandler: @escaping UDChangeHandler<UserDefaultable, URL>) -> UDKeyValueObservation { | |
return source.observe(URL.self, forKey: namespace(key), options: options, changeHandler: changeHandler) | |
} | |
} | |
// MARK: - DEMO ------------------------ | |
class MyDefaults: StringUserDefaultable, IntUserDefaultable { | |
enum StringDefaultKey: String { | |
case userName | |
} | |
enum IntDefaultKey: String { | |
case impressionCount | |
} | |
} | |
class MyClass { | |
let defaults: UserDefaultable | |
var observation: KeyValueObservation? | |
init(defaults: UserDefaultable) { | |
self.defaults = defaults | |
observation = MyDefaults.observe(for: .userName, source: defaults, options: [.new, .old]) { _, change in | |
print("Changed to", change.newValue ?? "nil", "from", change.oldValue ?? "nil") | |
} | |
} | |
} | |
print("UserDefaults ----------------") | |
UserDefaults.standard.reset() | |
let hoge = MyClass(defaults: UserDefaults.standard) | |
print("1") | |
MyDefaults.set("Pikachu", forKey: .userName) | |
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) // for OS bug | |
print("2") | |
MyDefaults.set("Pikachu", forKey: .userName) | |
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) // for OS bug | |
print("3") | |
MyDefaults.set("Eevee", forKey: .userName) | |
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) // for OS bug | |
print("4") | |
MyDefaults.set(nil, forKey: .userName) | |
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) // for OS bug | |
print("5") | |
hoge.observation = nil | |
MyDefaults.set("Pikachu", forKey: .userName) | |
print("\nUserDefaultsFake ----------------") | |
let defaultsFake = UserDefaultsFake() | |
let hogeFake = MyClass(defaults: defaultsFake) | |
print("1") | |
MyDefaults.set("Pikachu", forKey: .userName, source: defaultsFake) | |
print("2") | |
MyDefaults.set("Pikachu", forKey: .userName, source: defaultsFake) | |
print("3") | |
MyDefaults.set("Eevee", forKey: .userName, source: defaultsFake) | |
print("4") | |
MyDefaults.set(nil, forKey: .userName, source: defaultsFake) | |
print("5") | |
hogeFake.observation = nil | |
MyDefaults.set("Pikachu", forKey: .userName, source: defaultsFake) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment