UserDefaultsをProperty Wrapperでカッコよく使う

iOSプログラマなら誰もが3回は作っているUserDefaultsをいい感じに使う方法の解説です。

今回はProperty Wrapperを活用します。目標は以下のとおり。

*  あまりコードを書かずに使える
*  UserDefaultsで元々使える型も使えない型も同じように使える
*  型安全に使える（キャストせずに済む）
*  Optional<Int>も扱う（UserDefaultsの`integer(forKey:)`は値がないときnilではなく0を返してしまう）
*  Swift5.1以降対応
*  iOS11以降対応

# カッコよく使う例
アプリごとに用意するUserDefaultsアクセス部分の例です。

```swift
struct User : Codable, UserDefaultCompatible {
    var name: String
}

struct Settings {
    @UserDefault("user", default: User(name: "abc"))
    var user: User
    
    @UserDefault("userOptional", default: nil)
    var userOptional: User?
    
    @UserDefault("users", default: [])
    var users: [User]
    
    @UserDefault("userDictionary", default: [:])
    var userDictionary: [String: User]
    
    @UserDefault("int", default: 5)
    var int: Int

    @UserDefault("intOptional", default: nil)
    var intOptional: Int?
}
```

UserはCodableを実装した型です。
もちろんIntなどUserDefaultsが元々対応している型も保存できます。
Array, Dictionary, Optionalも扱うことができます。

* キー文字列の指定
* デフォルト値の指定
* プロパティの定義（名前と型の指定）

という最小限の実装でUserDefaultsを扱うことができます。

## ソースコード

説明はいらないソース見せろという場合は[Playgroundに貼り付けられるコード](https://gist.github.com/ryotapoi/5746c5e1103014efcea22cb026bbcd3f)をコピペするか、[UserDefaultCompatible](https://github.com/ryotapoi/UserDefaultCompatible) と [UserDefaultPropertyWrapper](https://github.com/ryotapoi/UserDefaultPropertyWrapper) をご確認ください。
記事では省略している部分も全て記載しています。

UserDefaultPropertyWrapperをSwiftPMで取り込んでもOKです。

# UserDefaultsをカッコよく使う部品を作る
工程は大きく分けてふたつあります。

1. UserDefaultsに読み書き可能な型の作成
2. UserDefaultsにアクセスするProperty Wrapperを作成

## UserDefaultsに読み書き可能な型の作成
UserDefaultsに読み書き可能な型（protocol）と、実際にその型を読み書きするコードは以下となります。

```swift
// このProtocolに準拠していればUserDefaultsに読み書きできる
protocol UserDefaultCompatible {
    init?(userDefaultObject: Any)
    func toUserDefaultObject() -> Any?
}

extension UserDefaults {
    // 読み込み
    func value<Value : UserDefaultCompatible>(type: Value.Type = Value.self, forKey key: String, default defaultValue: Value) -> Value {
        guard let object = object(forKey: key) else { return defaultValue }
        return Value(userDefaultObject: object) ?? defaultValue
    }

    // 書き込み
    func setValue<Value : UserDefaultCompatible>(_ value: Value, forKey key: String) {
        set(value.toUserDefaultObject(), forKey: key)
    }
}
```

`UserDefaultCompatible` を通して、UserDefaultsが標準で読み書き可能な型に変換することで任意の方を読み書きできるようにします。

補足として`func value(type:forKey:default:) -> Value`は引数のdefaultまたは戻り値で型を確定させれば、type引数が省略可能なようにしています。

### Codable/NSCoding対応

ここからUserDefaultsが標準で読み書き可能な型に変換する部分を作っていきます。

CodableはDataに変換可能です。
DataはUserDefaults読み書き可能なため、これを利用すればCodable準拠のstruct/enumを簡単に保存できます。

NSCodingもDataに変換可能なため、同様のことが可能です。

```swift
extension UserDefaultCompatible where Self : Codable {
    init?(userDefaultObject: Any) {
        // Data型で保存されているはず
        guard let data = userDefaultObject as? Data else { return nil }
        do {
            self = try JSONDecoder().decode(Self.self, from: data)
        } catch {
            return nil
        }
    }
    func toUserDefaultObject() -> Any? {
        // Data型に変換する
        try? JSONEncoder().encode(self)
    }
}

// NSCodingのコードは省略
```


### UserDefaultsが元々対応している型の対応

`Int`などUserDefaultsの標準機能で読み書き可能な型もUserDefaultCompatibleとして読み書きできると実装が楽になります。

`Int`はCodableに準拠しているため簡単にUserDefaultCompatibleに準拠させられます。
 
```swift
extension Int : UserDefaultCompatible {}
```

しかしこの場合**IntではなくJSONのDataに変換して保存する**ため `integer(forKey:)` で値が取り出せなくなります。
互換性を考慮する場合は以下の実装が必要です。

```swift
extension Int : UserDefaultCompatible {
    init?(userDefaultObject: Any) {
        // Self（ここではInt）にキャストする
        guard let userDefaultObject = userDefaultObject as? Self else { return nil }
        self = userDefaultObject
    }
    func toUserDefaultObject() -> Any? {
        // 保存時に変換する必要がないためselfをそのまま返す
        self
    }
}

// `Double` `Float` `Bool` `String` `Date` `Data` は `Int` と同じコードで対応可能。
// コードは省略。

// URLは特殊
extension URL : UserDefaultCompatible {
    init?(userDefaultObject: Any) {
        // NSKeyedArchiverでData化した値が保存されているはず
        guard let userDefaultObject = userDefaultObject as? Data else { return nil }
        guard let url = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(userDefaultObject) as? URL else { return nil }
        self = url
    }
    func toUserDefaultObject() -> Any? {
        // NSKeyedArchiverでDataに変換する
        try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
    }
}
```

`URL`だけは特殊です。
内部で NSKeyedArchiver/NSKeyedUnarchiver を使用しているらしく`func url(forKey defaultName: String) -> URL?`で読めるようにするにはNSKeydArchiver/Unarchiverを使うようにします。

### Array/Dictionaryの対応

ArrayとDictionaryの要素がUserDefaultsに対応した型の場合、そのArrayとDictionaryも読み書き可能です。
そのため要素がUserDefaultCompatibleであれば、要素を変換することで読み書き可能になります。

```swift
extension Array : UserDefaultCompatible where Element : UserDefaultCompatible {
    private struct UserDefaultCompatibleError : Error {}
    init?(userDefaultObject: Any) {
        guard let objects = userDefaultObject as? [Any] else { return nil }
        do {
            let values = try objects.map { (object: Any) -> Element in
                if let element = Element(userDefaultObject: object) {
                    return element
                } else {
                    throw UserDefaultCompatibleError()
                }
            }
            self = values
        } catch {
            return nil
        }
    }
    func toUserDefaultObject() -> Any? {
        map { $0.toUserDefaultObject() }
    }
}

// Dictionaryも似たようなコードで対応可能。コードは省略
```


### Optionalの対応

Optionalの要素がUserDefaultsに対応した型の場合、そのOptionalも読み書き可能です。
そのため要素がUserDefaultCompatibleであれば、要素を変換することで読み書き可能になります。

```swift
extension Optional : UserDefaultCompatible where Wrapped : UserDefaultCompatible {
    init?(userDefaultObject: Any) {
        self = Wrapped(userDefaultObject: userDefaultObject)
    }
    func toUserDefaultObject() -> Any? {
        flatMap { $0.toUserDefaultObject() }
    }
}
```

## UserDefaultsにアクセスするProperty Wrapperを作成
最後に、UserDefaultCompatibleを使い実際にUserDefaultsを読み書きするProperty Wrapperを作成します。
このProperty Wrapperは以下の機能を持ちます。

* UserDefaultsのキー文字列の指定
* キーに対応する値がない場合のデフォルト値の指定
* UserDefaultsの読み書き

```swift
@propertyWrapper
struct UserDefault<Value : UserDefaultCompatible> {
    private let key: String
    private let defaultValue: Value

    // 初期化時にキー文字列とデフォルト値を受け取る
    init(_ key: String, default defaultValue: Value) {
        self.key = key
        self.defaultValue = defaultValue
    }

    // PropertyWrapperでラップしたプロパティへのアクセスは
    // wrapperdValueプロパティへのアクセスに自動で置き換えられる
    var wrappedValue: Value {
        get {
            UserDefaults.standard.value(type: Value.self, forKey: key, default: defaultValue)
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: key)
        }
    }
}
```


# まとめ
以上により、カッコよく使う実装が完成しました。
サンプルでは一部省略していますが

* UserDefaultsが元々対応している`Int` `Double` `Float` `Bool` `String` `Date` `Data`
* `Codable` `NSCoding` に準拠した型
* 上記を要素にもつ `Array` `Dictionary` `Optional`

に対応しているため、そうそう困ることはないでしょう。

# 補足

## 検討①　キーはただの文字列

UserDefaultsに保存するときのキーはただの文字列を使用します。キー文字列を複数回タイプすることはほぼないため、別の型を用意するほどではないと考えました。
ただSettingsを分割してUserSettingsやFuntionSettingsなどを用意する場合は、キー文字列が重複していないかすぐ確認できるように、特定の型で一箇所にまとめておいてもよいかもしれません。

## 検討②　registerDefaultsは使わない

UserDefaultsには`func register(defaults registrationDictionary: [String : Any])`という便利機能があります。
初期値をDictionaryでまとめて渡すと、値がないときだけ書き込んでくれます。初回起動か気にせず呼び出せますし、バージョンアップ時は追加の項目だけ書き込んでくれます。
これに対応する機能も作りたかったのですが、キー文字列に対応する値の型を強制する（対応しない型ならコンパイルエラーにする）方法がわからなかったためProperty Wrapperに初期値を渡す作りにしました。

## 検討③　UnitTestをどうするか

上記例の`struct Settings`自体のテストは不要と考えています。そんなに不具合はないだろうと楽観視。

`var Settings.user: User`を使うテストをする場合は

```swift
protocol UserSetting {
    var user: User { get set }
}
```
を用意してUnitTestではMockを渡すくらいでしょうか？

## 検討④　`[Int?]`は使えません

実際動かしてみたところ「property list形式ではありません」といった感じのエラーが出てしまいました。（保存できるのはprroperty list形式なんですね。protocolの名前をそれっぽいものにしてもいいかも）

NSNullを使えばいけるかもしれませんが `Int?` でnilを保存するときにNSNullになっても面倒だし非対応ということにしました。

## `var projectedValue`

Property WrapperにはprojectedValueという（正式名称かは知らないけど）面白い機能があります。
`var projectedValue: Int { 3 }` と実装した場合 `settings.$user`とすると `3` が取得できます（戻り値の型は何でもいいらしい）。
iOS13以降限定でCombineの何かを返せば面白いかと思いましたが、その場合そもそもProperty Wrapperを使う必要もないなと思ったので今回この機能は使いませんでした。