【iOS】UserDefaultsをSwiftらしく使う

今回はUserDefaultsの話になります。
UserDefaultsは手軽にデータの永続化ができるため、多くの場面で利用される機能です。
私はアプリ設定の保存によく使います。

このUserDefaultsについて、Swift3.0で大きな変更がありました。
まずは、Swift3.0の変更をおさらいします。

----
##### 1. インスタンスの作成
```swift:変更前
NSUserDefaults.standardUserDefaults()
```
```swift:変更後
UserDefaults.standard
```
コードから見て分かるとおり、NSUserDefaultsからUserDefaultsに置き換わりました。
また、インスタンスの生成もstandardUserDefaults()メソッドからstandardプロパティに変更されています。

##### 2. 保存
```swift:変更前
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "boolKeyName")
NSUserDefaults.standardUserDefaults().setInt(1, forKey: "integerKeyName")
```
```swift:変更後
UserDefaults.standard.set(true, forKey: "boolKeyName")
UserDefaults.standard.set(1, forKey: "integerKeyName")
```
保存のためのメソッドは、**set(_:forKey:)**のみになりました。
コンパイラが第1パラメータの型を推測して、適切なメソッドが呼び出される仕組みになります。

##### 3. 読み込み
```swift:変更前
NSUserDefaults.standardUserDefaults().boolForKey("boolKeyName")
NSUserDefaults.standardUserDefaults().integerForKey("integerKeyName") 
```
```swift:変更後
UserDefaults.standard.bool(forKey: "boolKeyName")
UserDefaults.standard.integer(forKey: "integerKeyName")
```
メソッド名が少し短くなりました。

##### 4. 削除
```swift:変更前
NSUserDefaults.standardUserDefaults().removeObjectForKey("keyName")
```
```swift:変更後
UserDefaults.standard.removeObject(forKey: "keyName")
```

##### 5. 存在チェック
```swift:変更前
if NSUserDefaults.standardUserDefaults().objectForKey("KeyName") != nil { ... }
```
```swift:変更後
if UserDefaults.standard.object(forKey: "KeyName") != nil { ... }
```

##### 6. Synchronize
非推奨になりました。

# UserDefaultsの問題点
さて、記事のタイトルは**UserDefaultsをSwiftらしく使う**でしたが、まずはよく見かける方法でUserDefaultsを使ってみたいと思います。

```swift:
struct Constants {
        static let telKey = "tel"
}
```

```swift:
// 電話番号
let tel = "000-0000-0000"

// 保存
UserDefaults.standard.set(tel, forKey: Constants.telKey)

// 読み込み
UserDefaults.standard.string(forKey: Constants.telKey)
```

UserDefaultsのキー名は、保存時・読み込む時等、複数回使用される可能性が高いため、定数にするのが一般的だと思います。
しかしこの方法だと文字列定数を使っているため、打ち間違えが発生しやすい問題があります。
私はtel（電話番号）を **"tell(伝える)"**と打ち間違えしてしまうことがあります。
打ち間違えをレビューで指摘されると、ちょっと辛いですよね、、、

打ち間違えによるミスを防ぐために、定数で定義している文字列を**Enum**に変更すると、少し良い感じになります。


```swift:
struct Constants {
        enum User : String {
                case tel
        }
}
```

```swift:
// 電話番号
let tel = "000-0000-0000"

// 保存
UserDefaults.standard.set(tel, forKey: Constants.User.tel.rawValue)

// 読み込み
UserDefaults.standard.string(forKey: Constants.User.tel.rawValue)
```
Enumは列挙子にString型のRaw値「tel」を割り当てるとができるため、文字列で直接**"tel"**と記述する必要がなくなります。

# SwiftらしいUserDefaults
Enumを使い、打ち間違えによるエラーを防ぐことができましたが、キー名に「Constants.User.tel.rawValue」と記述するのは、冗長的でSwiftらしくありません。
これをプロトコル拡張で、より短く簡潔にすることができます。

```swift:
protocol KeyNamespaceable {
        func namespaced<T: RawRepresentable>(_ key: T) -> String
}

extension KeyNamespaceable {
    
        func namespaced<T: RawRepresentable>(_ key: T) -> String {
                return "\(Self.self).\(key.rawValue)"
        }
}

protocol StringDefaultSettable : KeyNamespaceable {
        associatedtype StringKey : RawRepresentable
}

extension StringDefaultSettable where StringKey.RawValue == String {

        func set(_ value: String, forKey key: StringKey) {
                let key = namespaced(key)
                UserDefaults.standard.set(value, forKey: key)
        }

        @discardableResult
        func string(forKey key: StringKey) -> String? {
                let key = namespaced(key)
                return UserDefaults.standard.string(forKey: key)
        }
}

extension UserDefaults : StringDefaultSettable {
        enum StringKey : String {
                case tel
        }
}
```

```swift:
// 電話番号
let tel = "000-0000-0000"

// 保存
UserDefaults.standard.set(tel, forKey: .tel) // キー名 UserDefaults.tel

// 読み込み
UserDefaults.standard.string(forKey: .tel) // キー名 UserDefaults.tel
```

UserDefaultsの保存、読み込みのコードがより短く簡潔になりました。

**"KeyNamespaceable"**は、UserDefaultsのキー名の衝突を防ぐためのプロトコルです。
KeyNamespaceableに適合させることで、キー名は「UserDefaults.tel」になりますが適合させないと、名前空間が含まれないただの「tel」になってしまうため、衝突が発生してしまう可能性が高いです。

**"StringDefaultSettable"**は、UserDefaultsへString型の値を「保存・読み込み」するためのプロトコルになります。
StringDefaultSettableの拡張メソッド **set(_:forKey:)**、**string(forKey:)**はUserDefaultsのAPIと同じになります。
このStringDefaultSettableをUserDefaultsに適合させることで、以下のようにSwiftらしいコードになります。

```swift:
UserDefaults.standard.set(tel, forKey: .tel)
UserDefaults.standard.string(forKey: .tel)
```

また、Int型やDouble型等の値をUserDefaultsへ保存・読み込ませたい場合は、以下のようなプロトコルを作ると同様のことができます。

```swift:
protocol IntegerDefaultSettable : KeyNamespaceable {...}
protocol DoubleDefaultSettable : KeyNamespaceable {...}
protocol FloatDefaultSettable : KeyNamespaceable {...}
protocol ObjectDefaultSettable : KeyNamespaceable {...}
protocol URLDefaultSettable : KeyNamespaceable {...}
protocol BoolDefaultSettable : KeyNamespaceable {...}
```

# さらにSwiftらしく
実際にアプリ開発をする際、例えば**"tel(電話番号)"**のような情報は、モデルクラスのプロパティとして持つことが多いかと思います。
しかし、幸いにもStringDefaultSettableプロトコルは適合先をUserDefaultsに限定していないため、以下のようなモデルクラスにも適合させることができます。

```swift:
struct User {
        let name: String
        let tel: String
}

extension User : StringDefaultSettable {
        enum StringKey : String {
                case name
                case tel
        }
}
```

UserDefaultsで**"tel"**を持っていても何の電話番号かコードから読み取ることができませんが、Userで**"tel"**を持てば、ユーザーの電話番号ということが明確になります。

# おわりに
プロトコルを使ったSwiftらしいUserDefaultsは、打ち間違えやキー名の衝突を防止し、短く簡潔にコードを書くことができます。
そのため、Swiftではプロトコルを積極的に使っていくべきかと思います。

## 参考
[Swift Eye for the Stringly Typed API](https://realm.io/news/tryswift-andyy-hope-swift-eye-stringly-typed-api/)