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

`model.foo = 123` したら自動でUserDefaultsにも書き込む奴。
SourceryとAssociatedObjectで出来たのでメモ。

## 問題

アプリの設定をこういうクラスに突っ込んでて、プロパティを更新したときにUserDefaultsに書き込むようにしていた。

```swift
class AppSettings {
    var foo: Int
    var bar: String
}
```

[SwiftyUserDefaults](https://github.com/radex/SwiftyUserDefaults)を使うと、UserDefaultsへの保存はこういう感じになる。

```swift 
// キーを定義
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では既に存在するクラスにはプロパティを追加できない。

```swift
class Foo {}

class Foo {
    var foo = 123 // エラー！
}

extension Foo {
    var bar = "bar" // エラー！
}
```

どうしたものか〜〜と調べていたら、Associated Objectって奴で無理やり実現できることがわかった。


### Associated Object

オブジェクトに紐付くオブジェクトを作れるしくみ。
動的にプロパティを生やす、みたいなことができる。

参考: https://qiita.com/fmtonakai/items/e9036dec4af2609b5715

これを使ってAppSettingにプロパティを生やせば良い！


## やってみる

テンプレートファイルを作る

```swift
// 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で展開するとこうなる↓

```swift
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に保存されるようになった。

```swift
appSettings.foo = 123
print(Defaults[.foo]) // 123

appSettings.bar = "yoyo"
print(Defaults[.bar]) // yoyo
```

めでたしめでたし。