Swift5.1のProperty Wrappersでより安全なUser Defaults参照を実現する

Swift5.1から導入される **Property Wrappers** ですが、[WWDC 2019のセッションで User Defaults とプロパティを関連づける例](https://developer.apple.com/videos/play/wwdc2019/402)が紹介されていました。

これをもとにProperty Wrappersの仕組みについてもう一歩踏み込みつつ、より安全な参照を実現する方法を考えてみたいと思います。

## 任意の型を扱うためのプロトコルを定義する

User Defaultsが標準で扱える型は `String`, `Int` などのプリミティブな型と、 `Array` や `Dictionary` のコレクションに制限されていますが、 `Data` 型に変換ができれば、任意の型を扱うことができます。これらを踏まえ、以下のようなプロトコルを定義します。

```swift:UserDefaultConvertible.swift
protocol UserDefaultConvertible {
    init?(with object: Any)
    func object() -> Any?
}
```

## User Defaultsと関連づけるProperty Wrappers

User Defaultsと関連づけるProperty WrapperのGeneric Type(下記の例の `Value`)は、先述の `UserDefaultConvertible` への準拠を要求します。

```swift:UserDefaultPropertyWrapper.swift
@propertyWrapper
struct UserDefault<Value: UserDefaultConvertible> {
    let key: String
    let defaultValue: Value
    
    init(_ key: String, defaultValue: Value) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    var value: Value {
        get {
            if
                let object = UserDefaults.standard.object(forKey: self.key),
                let value = Value(with: object)
            {
                return value
            } else {
                return self.defaultValue
            }
        } set {
            if let object = newValue.object() {
                UserDefaults.standard.set(object, forKey: self.key)
            } else {
                UserDefaults.standard.removeObject(forKey: self.key)
            }
        }
    }
}
```

あとは対応したい型に対して `UserDefaultConvertible` への準拠を行なっていくだけです。 `Int` 型の例を示します。

```swift:Int+UserDefaultConvertible.swift
extension Int: UserDefaultConvertible {
    init?(with object: Any) {
        guard let value = object as? Int else {
            return nil
        }
        self = value
    }
    
    func object() -> Any? {
        return self
    }
}
```

任意の型の `UserDefaultConvertible` 準拠は、愚直に実装するより `Codable` に準拠を前提とした以下のような拡張を用意すると良いでしょう。

```swift:UserDefaultConvertible.swift
extension UserDefaultConvertible where Self: Codable {
    init?(with object: Any) {
        guard
            let data = object as? Data,
            let value = try? JSONDecoder().decode(Self.self, from: data)
        else {
            return nil
        }
        
        self = value
    }
    
    func object() -> Any? {
        return try? JSONEncoder().encode(self)
    }
}
```

`Optional` も扱えるようにしておきましょう。
(こういう場面でConditional Conformanceが生きますね！)

```swift:Optional+UserDefaultConvertible.swift
extension Optional: UserDefaultConvertible where Wrapped: UserDefaultConvertible {
    init?(with object: Any) {
        guard let value = Wrapped(with: object) else {
            return nil
        }
        self = .some(value)
    }
    
    func object() -> Any? {
        switch self {
        case .some(let value):
            return value.object()
        case .none:
            return nil
        }
    }
}
```

## ここまでの実装でできること

さて、これでどのような書き方ができるかというと...

```swift
// User Defaultsへのストアため Codable, UserDefaultConvertible に準拠
struct UserProfile: Codable, UserDefaultConvertible {
    var firstName: String
    var lastName: String

    enum Gender: String, Codable {
        case male
        case female
        case other
    }
    var gender: Gender
}

class App {
    static let shared = App()
    private init() {}

    // Data型に変換し、User Defaultsに "loginUserProfile" という名前でストア
    @UserDefault("loginUserProfile", defaultValue: nil)
    var userProfile: UserProfile?
}

// Usage
if let userProfile = App.shared.userProfile {
    ...
}
```

任意の型を User Defaultsに保存・参照できるようになりました！

## 型安全かつtypoしにくい設計にする

いまの設計だと、User Defaultsへの参照キーは文字列リテラルで指定することになるため、以下の懸念点があります。

* 異なる情報に対し、同じ名前の参照キーを命名できてしまう
* ストアされている型と異なる型を指定して取り出しができてしまう(失敗する)

そこで[SwiftyUserDefaults](https://github.com/radex/SwiftyUserDefaults)で採られている方法を参考に、より安全な設計に変更してみます。

### 値の型情報を持つキー型を定義する

SwiftyUserDefaultsの実装のように、以下のようなクラスを用意します。

```swift:UserDefaultTypedKey.swift
class UserDefaultTypedKeys {
    init() {}
}

class UserDefaultTypedKey<T>: UserDefaultTypedKeys {
    let key: String
    
    init(_ key: String) {
        self.key = key
        super.init()
    }
}
```

### `UserDefaultTypedKey<T>` に対応したProperty Wrapperに修正する

`String` で持っていた参照キーを `UserDefaultTypedKey<T>` に変更します。

```swift
@propertyWrapper
struct UserDefault<Value: UserDefaultConvertible> {
    let typedKey: UserDefaultTypedKey<Value>
    let defaultValue: Value
    
    init(_ typedKey: UserDefaultTypedKey<Value>, defaultValue: Value) {
        self.typedKey = typedKey
        self.defaultValue = defaultValue
    }
    
    var value: Value {
        get {
            if
                let object = UserDefaults.standard.object(forKey: self.typedKey.key),
                let value = Value(with: object)
            {
                return value
            } else {
                return self.defaultValue
            }
        } set {
            if let object = newValue.object() {
                UserDefaults.standard.set(object, forKey: self.typedKey.key)
            } else {
                UserDefaults.standard.removeObject(forKey: self.typedKey.key)
            }
        }
    }
}
```

### 参照キーの定義と利用

`UserDefaultTypedKeys` を拡張し、User Defaultsへの参照キーを `static` なプロパティで定義します。その際Genericパラメータに値の型、イニシャライザに実際の参照キーを指定します。

```swift
extension UserDefaultTypedKeys {
    static let loginUserProfile = UserDefaultTypedKey<UserProfile?>("loginUserProfile")
}
```

これで必要なものは整いました。
先ほどの実装例は以下のようになります。

```swift
class App {
    static let shared = App()
    private init() {}

    @UserDefault(.loginUserProfile, defaultValue: nil)
    var userProfile: UserProfile?
}
```

微々たる変化ではありますが、

* 文字列リテラルで記述する必要がない(補完が効く！)
* 型が一致していないとコンパイルエラーになる

という効果が生まれます。
`UserDefaultTypedKeys` の中でキー重複のない、型制約のついたUser Defaultsが表現できますね！

## おまけ

`Array` の場合、各 `Element` が `UserDefaultConvertible` 準拠であり、全要素が相互変換できないと `Error` にするという仕様が良さそうです。

### `Array` を `UserDefaultConvertible` 準拠にする

```swift:Array+UserDefaultConvertible.swift
extension Array: UserDefaultConvertible where Element: UserDefaultConvertible {
    private struct Error: Swift.Error {}
    
    init?(with object: Any) {
        guard let array = object as? [Any] else {
            return nil
        }
        
        guard let value = try? array.map({ (object) -> Element in
            if let element = Element(with: object) {
                return element
            } else {
                throw Error()
            }
        }) else {
            return nil
        }
        
        self = value
    }
    
    func object() -> Any? {
        return try? self.map { (element) -> Any in
            if let object = element.object() {
                return object
            } else {
                throw Error()
            }
        }
    }
}
```

同じ要領で `Dictionary` も対応できるはずです。