model.foo = 123
したら自動でUserDefaultsにも書き込む奴。
SourceryとAssociatedObjectで出来たのでメモ。
問題
アプリの設定をこういうクラスに突っ込んでて、プロパティを更新したときにUserDefaultsに書き込むようにしていた。
class AppSettings {
var foo: Int
var bar: String
}
SwiftyUserDefaultsを使うと、UserDefaultsへの保存はこういう感じになる。
// キーを定義
extension DefaultsKeys {
static let foo = DefaultsKey<Int>("foo", defaultValue: 0)
static let bar = DefaultsKey<String>("bar", defaultValue: "BAR")
}
// fooのセッター
func setFoo(value: Int) {
appSettings.foo = value
Defaults[.foo] = value
}
// barのセッター
func setBar(value: String) {
appSettings.bar = value
Defaults[.bar] = value
}
設定が増えてくると、似たようなコードが大量に生まれてツラい……引数の型はいちいち書き換えなきゃいけないし……。
AppSettingsのプロパティを更新した時に、自動でUserDefaultsに保存できないかな、ということで試してみた。
使った技術
Sourcery
Swiftのコードをテンプレートから自動生成するやつ。メタプロできる。
DefaultsKeysのキーを使って、AppSettingsにプロパティを追加できればよい。
しかし、Swiftでは既に存在するクラスにはプロパティを追加できない。
class Foo {}
class Foo {
var foo = 123 // エラー!
}
extension Foo {
var bar = "bar" // エラー!
}
どうしたものか〜〜と調べていたら、Associated Objectって奴で無理やり実現できることがわかった。
Associated Object
オブジェクトに紐付くオブジェクトを作れるしくみ。
動的にプロパティを生やす、みたいなことができる。
参考: https://qiita.com/fmtonakai/items/e9036dec4af2609b5715
これを使ってAppSettingにプロパティを生やせば良い!
やってみる
テンプレートファイルを作る
// AppSettingsAutoSave.stencil
import Foundation
import SwiftyUserDefaults
// Associated Object設定用便利メソッド
private func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {
return objc_getAssociatedObject(object, key) as? T
}
private func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
// Associated Objectのキーを作る
{% for variable in type.DefaultsKeys.staticVariables %}
private var {{variable.name}}Key: Void?
{% endfor %}
extension AppSettings {
{% for variable in type.DefaultsKeys.staticVariables %}
var {{variable.name}}: {{variable.typeName|replace:"DefaultsKey<",""|replace:">",""}} {
get {
return getAssociatedObject(self, &{{variable.name}}Key) ?? Defaults[.{{variable.name}}]
}
set {
setRetainedAssociatedObject(self, &{{variable.name}}Key, newValue)
Defaults[.{{variable.name}}] = newValue
}
}
{% endfor %}
}
これをSourceryで展開するとこうなる↓
private var fooKey: Void?
private var barKey: Void?
private var bazKey: Void?
extension AppSettings {
var foo: Int {
get {
return getAssociatedObject(self, &fooKey) ?? Defaults[.foo]
}
set {
setRetainedAssociatedObject(self, &fooKey, newValue)
Defaults[.foo] = newValue
}
}
var bar: String {
get {
return getAssociatedObject(self, &barKey) ?? Defaults[.bar]
}
set {
setRetainedAssociatedObject(self, &barKey, newValue)
Defaults[.bar] = newValue
}
}
var baz: Bool {
get {
return getAssociatedObject(self, &bazKey) ?? Defaults[.baz]
}
set {
setRetainedAssociatedObject(self, &bazKey, newValue)
Defaults[.baz] = newValue
}
}
}
こうすることで、AppSettingsのプロパティを更新すると自動的にUserDefaultsに保存されるようになった。
appSettings.foo = 123
print(Defaults[.foo]) // 123
appSettings.bar = "yoyo"
print(Defaults[.bar]) // yoyo
めでたしめでたし。