Skip to content

Instantly share code, notes, and snippets.

@insidegui
Created January 15, 2017 02:40
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 insidegui/78f08b21517fe3b699f91a3c476b9e5b to your computer and use it in GitHub Desktop.
Save insidegui/78f08b21517fe3b699f91a3c476b9e5b to your computer and use it in GitHub Desktop.
A better way to "store value types" in NSUserDefaults or NSUbiquitousKeyValueStore
/**
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