【Swift】プロパティラッパ(Property Wrapper)で UserDefaults へのアクセスを簡単にする【Xcode】

iOS エンジニアなら一度は使ったことがあるであろう UserDefaults。

今まで私は以下のようなクラスを作って、UserDefaultsからデータを取り出したり格納したりしていました。

// 
// UserDefaults へのアクセスをラップするクラス
class MyUserDefaults{

    // Bool 型の値をUserDefaultsで管理する
    static var sampleFlag: Bool{
        get{
            return UserDefaults.standard.object(forKey: "sampleFlag") as? Bool ?? false
        }set{
            UserDefaults.standard.set(newValue, forKey: "sampleFlag")
        }
    }
    
    // String 型の値をUserDefaultsで管理する
    static var sampleData: String{
        get{
            return UserDefaults.standard.object(forKey: "sampleData") as? String ?? ""
        }set{
            UserDefaults.standard.set(newValue, forKey: "sampleData")
        }
    }
}

こんなふうに、UserDefaults で管理したいデータごとにプロパティを作成していました。

このプロパティは計算型プロパティ(Computed property)として、ゲッターとセッター内で UserDefaults に対する操作を行っています。

ですがこのやり方だと、プロパティが増えるに伴って、同じような「get」「set」という構文が並ぶことになり、非常に冗長になってしまいます。いわゆるボイラープレートですね。

Swift 5.1 から導入された Property Wrapper を使うことで、もう少しスマートに実装することができるようになります。

本記事では、私が最近使用している UserDefaults のラッパーを備忘録として残しておきます。

Property Wrapper とは

Property Wrapper を簡単に説明すると、以下のような感じになると思います。

「プロパティがどのように値を返却するか、どのように初期値を決定するか、値がnilだった場合のデフォルト値はどうするか?といったプロパティの性質をあらかじめ定義しておき、その定義を任意のプロパティ に適用することができる」

Property Wrapper の詳細は以下の Proposal に載っています。

https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md

Property Wrapper を使用して UserDefaults へのアクセスをラップする

基本的には以下のサイトをベースにさせていただいています。

https://swiftsenpai.com/swift/create-the-perfect-userdefaults-wrapper-using-property-wrapper/

//
/// UserDefaults へのアクセスをラップするための PropertyWrapper  定義
@propertyWrapper
struct MyUserDefaults {
    private let key: String
    private let defaultValue: T
    private let userdef: UserDefaults

    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
        userdef = UserDefaults.standard
    }

    var wrappedValue: T {
        get {
            // UserDefaults にデータがなければデフォルト値を返す
            guard let data = userdef.object(forKey: key) as? Data else {
                return defaultValue
            }

            // データを取得
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        } set {
            let data = try? JSONEncoder().encode(newValue)
            userdef.set(data, forKey: key)
        }
    }
}

/// UserDefaults に格納するプロパティを定義する構造体
struct AppData {
    /// 初回起動フラグ
    @MyUserDefaults(key: "FirstLaunchFlag", defaultValue: true)
    static var firstLaunchFlag: Bool
    
    /// Bool や Int などの基本型以外も格納できる(Codable に準拠している場合のみ)
    @MyUserDefaults(key: "hoge", defaultValue: nil)
    static var hoge: Hoge?
}


実際に UserDefaults に値を格納するためのコードは以下のような感じです。

//
// Bool型のプロパティを格納
AppData.firstLaunchFlag = false

/// Codableに準拠していれば構造体も格納できる
struct Hoge: Codable{
    var flag = false
    var data = "data"
    
    init(flag: Bool, data: String){
        self.flag = flag
        self.data = data
    }
}

let hoge = Hoge(flag: false, data: "data")
AppData.hoge = hoge