Skip to content

Instantly share code, notes, and snippets.

@krimpedance
Created September 10, 2018 15:22
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 krimpedance/27a2e3c5a7d301b4b468cca944279357 to your computer and use it in GitHub Desktop.
Save krimpedance/27a2e3c5a7d301b4b468cca944279357 to your computer and use it in GitHub Desktop.
UserDefaults * Swifty * Observable * Testable
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