LoginSignup
0
0

More than 3 years have passed since last update.

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

Posted at

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

めでたしめでたし。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0