Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@cfloisand
Last active February 8, 2024 18:46
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cfloisand/ba9eb5b661a7dda494bb45f28cdb7e0a to your computer and use it in GitHub Desktop.
Save cfloisand/ba9eb5b661a7dda494bb45f28cdb7e0a to your computer and use it in GitHub Desktop.
A better UserDefaults in Swift using Key-Value Observing
import Foundation
//# MARK: - FSUserDefaults
class FSUserDefaults: NSObject {
private var _observer: FSDefaultsObserver!
override init() {
super.init()
_observer = FSDefaultsObserver(object: self)
}
}
//# MARK: - FSDefaultsObserver
fileprivate class FSDefaultsObserver: NSObject {
@objc weak var observedObject: NSObject!
var observations: [(String, UnsafeMutableRawPointer)] = []
enum ObjectType {
case string, array, dictionary, data, url
case any
}
enum ValueType: Character {
case boolean = "b"
case integer = "i"
case float = "f"
case double = "d"
case url = "u"
case object = "@"
}
let contextPointerSize = MemoryLayout<ValueType.RawValue>.size
let contextPointerAlignment = MemoryLayout<ValueType.RawValue>.alignment
init(object: NSObject) {
observedObject = object
super.init()
let keys = UserDefaults.standard.dictionaryRepresentation().keys
let existingKeyPaths = keys.filter({ key -> Bool in
return key.hasPrefix(#keyPath(observedObject))
})
var propertyCount: UInt32 = 0
if let properties = class_copyPropertyList(object_getClass(observedObject), &propertyCount) {
for idx in 0..<propertyCount {
let property = properties[Int(idx)]
let name = String(cString: property_getName(property))
let attr = String(cString: property_getAttributes(property)!)
var keyPath = #keyPath(observedObject)
keyPath = keyPath.appending(".").appending(name)
var value: Any?
let typeIdx = attr.index(attr.startIndex, offsetBy: 1)
let type = attr[typeIdx]
let context = UnsafeMutableRawPointer.allocate(bytes: contextPointerSize, alignedTo: contextPointerAlignment)
switch type {
case "c", "B":
context.storeBytes(of: ValueType.boolean, as: ValueType.self)
value = UserDefaults.standard.bool(forKey: keyPath)
case "s", "i", "l", "q", "S", "I", "L", "Q":
context.storeBytes(of: ValueType.integer, as: ValueType.self)
value = UserDefaults.standard.integer(forKey: keyPath)
case "f":
context.storeBytes(of: ValueType.float, as: ValueType.self)
value = UserDefaults.standard.float(forKey: keyPath)
case "d":
context.storeBytes(of: ValueType.double, as: ValueType.self)
value = UserDefaults.standard.double(forKey: keyPath)
case "@":
let objectType = __objectType(attr)
if objectType == .url {
context.storeBytes(of: ValueType.url, as: ValueType.self)
} else {
context.storeBytes(of: ValueType.object, as: ValueType.self)
}
switch objectType {
case .string: value = UserDefaults.standard.string(forKey: keyPath)
case .array: value = UserDefaults.standard.array(forKey: keyPath)
case .dictionary: value = UserDefaults.standard.dictionary(forKey: keyPath)
case .data: value = UserDefaults.standard.data(forKey: keyPath)
case .url: value = UserDefaults.standard.url(forKey: keyPath)
case .any: value = UserDefaults.standard.object(forKey: keyPath)
}
default:
assertionFailure("[FSUserDefaults] Unhandled property type.")
}
// NOTE(christian): If current property's key path is not contained in the UserDefaults' dictionary
// representation, it has not yet been set. In this case, don't set it's value in order to preserve
// default/init values.
if let val = value, existingKeyPaths.contains(keyPath) {
observedObject.setValue(val, forKey: name)
}
addObserver(self, forKeyPath: keyPath, options: [.new], context: context)
observations.append((keyPath, context))
}
free(properties)
}
}
deinit {
for obs in observations {
removeObserver(self, forKeyPath: obs.0, context: obs.1)
obs.1.deallocate(bytes: contextPointerSize, alignedTo: contextPointerAlignment)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let type = context?.load(as: ValueType.self) {
let value = change![.newKey]
switch type {
case .boolean: UserDefaults.standard.set(value as! Bool, forKey: keyPath!)
case .integer: UserDefaults.standard.set(value as! Int, forKey: keyPath!)
case .float: UserDefaults.standard.set(value as! Float, forKey: keyPath!)
case .double: UserDefaults.standard.set(value as! Double, forKey: keyPath!)
case .url: UserDefaults.standard.set((value as! URL), forKey: keyPath!)
case .object: UserDefaults.standard.set(value, forKey: keyPath!)
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
private func __objectType(_ attr: String) -> ObjectType {
if attr.contains("NSString") {
return .string
} else if attr.contains("NSArray") {
return .array
} else if attr.contains("NSDictionary") {
return .dictionary
} else if attr.contains("NSData") {
return .data
} else if attr.contains("NSURL") {
return .url
} else {
return .any
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment