プロパティを更新したら自動でUserDefaultsに書き込む

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

https://github.com/krzysztofzablocki/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

めでたしめでたし。