NSUserDefaultsへのアクセスを簡単にしてくれるSwiftDefaultsというライブラリを作成しました。
SwiftDefaultsという名前のライブラリです。
SwiftDefaultsでできること
表題の通りNSUserDefaultsにプロパティー形式でアクセスできるようになります。
文字列でキーを指定する必要がなくなるのでTypoの心配もありません。
import SwiftDefaults
class MyDefaults: SwiftDefaults {
dynamic var value: String? = "10"
dynamic var value2: String = "10"
dynamic var value3: Int = 1
}
// NSUserDefaultsの値の取得
print(MyDefaults().value) // "10"
print(MyDefaults().value2) // "10"
// NSUserDefaultsへの保存
MyDefaults().value2 = "2"
print(MyDefaults().value2) // "2"
print(MyDefaults().value3) // 1
プロパティーに与えた初期値がNSUserDefaultsのデフォルト値になります。
import SwiftDefaults
class MyDefaults: SwiftDefaults {
dynamic var value: String? = "10" // valueのデフォルト値は10になる
}
もしこのライブラリを使わない場合は上で挙げた事を実現する為に以下のように書く必要があります。(実際はラッパークラスを書いてもう少し見やすくすると思いますが)
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.registerDefaults([
"value": "10",
"value2": "10",
"value3": 1
])
print(userDefaults.stringForKey("value"))
print(userDefaults.stringForKey("value2")!)
userDefaults.setObject("2", forKey: "value2")
userDefaults.synchronize()
print(userDefaults.stringForKey("value2")!)
print(userDefaults.integerForKey("value3"))
実装内容
短いライブラリなので実装方法も軽く触れてみます。
ライブラリ本体のコードは以下の通りです。
import Foundation
public class SwiftDefaults: NSObject {
let userDefaults = NSUserDefaults.standardUserDefaults()
public override init() {
super.init()
registerDefaults()
setupProperty()
addObserver()
}
deinit {
removeObserver()
}
}
extension SwiftDefaults {
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let keyPath = keyPath {
userDefaults.setObject(change?["new"], forKey: keyPath)
userDefaults.synchronize()
}
}
}
extension SwiftDefaults {
private func registerDefaults() {
let dic = propertyNames.reduce([String:AnyObject]()) { (var dic, key) -> [String:AnyObject] in
dic[key] = valueForKey(key)
return dic
}
userDefaults.registerDefaults(dic)
}
private func setupProperty() {
propertyNames.forEach {
setValue(userDefaults.objectForKey($0), forKey: $0)
}
}
private func addObserver() {
propertyNames.forEach {
addObserver(self, forKeyPath: $0, options: .New, context: nil)
}
}
private func removeObserver() {
propertyNames.forEach {
removeObserver(self, forKeyPath: $0)
}
}
private var propertyNames: [String] {
return Mirror(reflecting: self).children.flatMap { $0.label }
}
}
NSUserDefaultsの値を取得する仕組み
「MyDefaults().value」という形式でNSUserDefaultsの値を取得できる仕組みですが、ここは最初に値をセットする事で実現しています。
具体的にはSwiftDefaultsのsetupProperty
メソッド内で各プロパティーにNSUserDefaultsの値を詰め込んでいます。
Mirrorオブジェクトを使えばSwiftDefaultsのプロパティー一覧を取得できるので、それを使ってプロパティー1つ1つにNSUserDefaultsの値をセットしています。
private var propertyNames: [String] {
return Mirror(reflecting: self).children.flatMap { $0.label }
}
private func setupProperty() {
propertyNames.forEach {
setValue(userDefaults.objectForKey($0), forKey: $0)
}
}
NSUserDefaultsに値を保存する仕組み
値の保存はKVOを使って実現しています。
具体的な実装箇所としてはSwiftDefaultsのaddObserver
メソッドとobserveValueForKeyPath
メソッドです。
KVOを使えばプロパティーへの値の代入時にobserveValueForKeyPath
メソッドが呼ばれるようになります。
それを利用してobserveValueForKeyPath
メソッド内でNSUserDefaultsに値を保存するようにしています。
private func addObserver() {
propertyNames.forEach {
addObserver(self, forKeyPath: $0, options: .New, context: nil)
}
}
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let keyPath = keyPath {
userDefaults.setObject(change?["new"], forKey: keyPath)
userDefaults.synchronize()
}
}
NSUserDefaultsのデフォルト値を登録する仕組み
NSUserDefaultsのデフォルト値の登録はregisterDefaults
メソッド内で行っています。
ここでもプロパティー名一覧を使ってDictionaryを作成、それをuserDefaults.registerDefaults
に渡すことでデフォルト値をセットしています。
private func registerDefaults() {
let dic = propertyNames.reduce([String:AnyObject]()) { (var dic, key) -> [String:AnyObject] in
dic[key] = valueForKey(key)
return dic
}
userDefaults.registerDefaults(dic)
}
今後対応したい事
Int?やBool?と言ったObjective-cでプリミティブなクラスのOptional型への対応をしたいと思います。
現状SwiftDefaultsではdynamicを使っているのでObjective-cでプリミティブだったクラスのOptional型を扱えません。
dynamicはKVOを使う為に付けてます。
対応方法としてはRealmのRealmOptionalのようにラップするクラスを作るのが良さそうな気がしてます。
import SwiftDefaults
class MyDefaults: SwiftDefaults {
dynamic var value: Int? = 1 // これはエラーになる
}