Created
January 15, 2017 02:40
-
-
Save insidegui/78f08b21517fe3b699f91a3c476b9e5b to your computer and use it in GitHub Desktop.
A better way to "store value types" in NSUserDefaults or NSUbiquitousKeyValueStore
This file contains 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
/** | |
A better way to "store value types" in NSUserDefaults or NSUbiquitousKeyValueStore | |
*/ | |
import Foundation | |
/// ValueCoder is a class that can encode values of a specific type via NSCoding | |
protocol ValueCoder: NSObjectProtocol, NSCoding { | |
/// The type this coder can encode/decode | |
associatedtype ValueType | |
/// When initialized with `init(value:...)`, contains the value to be encoded, when initialized with `init(coder:...`, contains the decoded value | |
var value: ValueType { get } | |
/// Initialize with the specified value to be encoded later using `NSKeyedArchiver` | |
init(value: ValueType) | |
} | |
/// Errors thrown by `KeyValueStore`'s `readValues` | |
enum KeyValueStoreError: Error { | |
case dataNotFound | |
case invalidData | |
} | |
/// Implemented by objects that can store other objects in key/value pairs (NSUserDefaults and NSUbiquitousKeyValueStore implement this) | |
protocol KeyValueStore { | |
func set(_ object: Any?, forKey: String) | |
func object(forKey: String) -> Any? | |
/// Store an array of values of type T, encoded with the `ValueCoder` type specified, under the key specified | |
func store<T, C: ValueCoder>(_ values: [T], using encoder: C.Type, forKey key: String) where C.ValueType == T | |
/// Read an array of values of type T stored under the specified key, decoded with the provided `ValueCoder` type | |
func readValues<T, C: ValueCoder>(forKey key: String, using decoder: C.Type) throws -> [T] where C.ValueType == T | |
/// Registers an observer that gets notified when the KeyValueStore changes, the block will get an array of the keys that changed | |
func addChangesObserver(_ block: @escaping (_ changedKeys: [String]) -> ()) -> NSObjectProtocol | |
/// Unregisters an observer registered with `addChangesObserver` | |
func removeChangesObserver(_ observer: NSObjectProtocol) | |
} | |
extension KeyValueStore { | |
func store<T, C: ValueCoder>(_ values: [T], using encoder: C.Type, forKey key: String) where C.ValueType == T { | |
let encodedValues = values.map({ encoder.init(value: $0) }) | |
let data = NSKeyedArchiver.archivedData(withRootObject: encodedValues) | |
set(data, forKey: key) | |
} | |
func readValues<T, C: ValueCoder>(forKey key: String, using decoder: C.Type) throws -> [T] where C.ValueType == T { | |
guard let data = object(forKey: key) as? Data else { | |
throw KeyValueStoreError.dataNotFound | |
} | |
guard let encoded = NSKeyedUnarchiver.unarchiveObject(with: data) as? [C] else { | |
throw KeyValueStoreError.invalidData | |
} | |
return encoded.map({ $0.value }) | |
} | |
func removeChangesObserver(_ observer: NSObjectProtocol) { | |
NotificationCenter.default.removeObserver(observer) | |
} | |
} | |
extension NSUbiquitousKeyValueStore: KeyValueStore { | |
func addChangesObserver(_ block: @escaping (_ changedKeys: [String]) -> ()) -> NSObjectProtocol { | |
let name = NSUbiquitousKeyValueStore.didChangeExternallyNotification | |
return NotificationCenter.default.addObserver(forName: name, object: self, queue: OperationQueue.main) { notification in | |
if let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] { | |
block(changedKeys) | |
} else { | |
block([]) | |
} | |
} | |
} | |
} | |
extension UserDefaults: KeyValueStore { | |
func addChangesObserver(_ block: @escaping ([String]) -> ()) -> NSObjectProtocol { | |
let name = UserDefaults.didChangeNotification | |
return NotificationCenter.default.addObserver(forName: name, object: self, queue: OperationQueue.main) { notification in | |
block([]) | |
} | |
} | |
} | |
/*************************************************************************************** | |
USAGE EXAMPLE: | |
***************************************************************************************/ | |
final class PeopleStorage { | |
private let store: KeyValueStore | |
init(store: KeyValueStore) { | |
self.store = store | |
} | |
func store(_ people: [Person]) { | |
store.store(people, using: PersonCoder.self, forKey: "people") | |
} | |
func retrievePeople() -> [Person] { | |
do { | |
return try store.readValues(forKey: "people", using: PersonCoder.self) | |
} catch { | |
print("Error reading people: \(error)") | |
return [] | |
} | |
} | |
} | |
final class PersonCoder: NSObject, ValueCoder { | |
typealias ValueType = Person | |
let value: Person | |
init(value: ValueType) { | |
self.value = value | |
} | |
init?(coder aDecoder: NSCoder) { | |
guard let name = aDecoder.decodeObject(forKey: "name") as? String, | |
let age = aDecoder.decodeObject(forKey: "age") as? Int | |
else { | |
return nil | |
} | |
self.value = Person(name: name, age: age) | |
} | |
func encode(with aCoder: NSCoder) { | |
aCoder.encode(value.name, forKey: "name") | |
aCoder.encode(NSNumber(value: value.age), forKey: "age") | |
} | |
} | |
struct Person { | |
let name: String | |
let age: Int | |
} | |
let people = [Person(name: "Guilherme", age: 24), | |
Person(name: "Tim", age: 55), | |
Person(name: "John", age: 36)] | |
let helper = PeopleStorage(store: UserDefaults.standard) | |
helper.store(people) | |
helper.retrievePeople() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment