UserDefaultsの概要と操作方法(Swift)

## 「UserDefaults」とは？

ユーザーのデフォルトデータベースへのインターフェイスです。
アプリの起動時にキーと値のペアを永続的に保存します。

一言でいうと「簡単に使えるキーバリュー型のDB」です。

## UserDefaultsを使う目的

アプリ起動時にデフォルトの状態や動作方法を決定するために使います。
キーと値のペアを簡単に永続化できるため、ちょっとした設定の保存などに向いています。

アプリを削除するとUserDefaults内のデータも全て消えるため、重要なデータをUserDefaultsのみに持たせるべきではありません。

## UserDefaultsの使用例

- ユーザーが好みの測定単位を指定する(cmやmなど)
- ユーザーがメディアの初期再生速度を指定する

## 環境

- Swift：4.2.1
- Xcode：10.1 (10B61)

## CRUD操作

キーは文字列である必要があります。

### 追加・更新

追加と更新は同じメソッドを使います。
値に応じたオーバーロードメソッドが呼ばれます。

```swift
UserDefaults.standard.set({値}, forKey: {キー})
```

### 取得

追加・更新時と異なり、オーバーロードメソッドがないため、値によってメソッドを呼び分ける必要があります。

```swift
UserDefaults.standard.object(forKey: {キー})
UserDefaults.standard.string(forKey: {キー})
UserDefaults.standard.array(forKey: {キー})
UserDefaults.standard.dictionary(forKey: {キー})
UserDefaults.standard.data(forKey: {キー})
UserDefaults.standard.stringArray(forKey: {キー})
UserDefaults.standard.integer(forKey: {キー})
UserDefaults.standard.float(forKey: {キー})
UserDefaults.standard.double(forKey: {キー})
UserDefaults.standard.bool(forKey: {キー})
UserDefaults.standard.url(forKey: {キー})
```

用意されていない型(例： `Date` 型)を取得したい場合、まず `UserDefaults.standard.object(forKey:)` で取得し、対象の型でキャストする必要があります。

```swift
private func load(key: String) -> Date? {
    let value = UserDefaults.standard.object(forKey: key)
    guard let date = value as? Date else {
        return nil
    }
    return date
}
```

### 削除

削除は値によってメソッドを呼び分ける必要がありません。

```swift
UserDefaults.standard.removeObject(forKey: {キー})
```

### 全削除

UserDefaultsの値を全削除する処理は標準で備わっていません。
以下のように `UserDefaults` クラスを拡張することで実現できます。

```swift:UserDefaults+Ex.swift
extension UserDefaults {
    func removeAll() {
        dictionaryRepresentation().forEach { removeObject(forKey: $0.key) }
    }
}
```

```swift:使い方
UserDefaults.standard.removeAll()
```

全削除処理を用意すると、UserDefaultsの単体テスト時に便利です。

ただし、簡単に実装できるのにAppleが用意していない理由は、不用意に実行して値が全削除されないようにするためかもしれません。
私はUserDefaultsを拡張せず、クライアントクラスを噛ませてその中で全削除処理を実装しています。
(いつか別記事で紹介したいです)

## 応用

### 自作した構造体などを扱う

自作した構造体などは、そのままだとUserDefaultsに追加できません。

例えば、以下のような構造体があるとします。

```swift:Monster.swift
struct Monster: Equatable {
    let name: String
    let description: String
}
```

そのままUserDefaultsに追加しようとすると、以下の実行時エラーが発生します。

```swift
let monster = Monster(name: "uhooi", description: "ゆかいな　みどりの　せいぶつ。")
UserDefaults.standard.set(monster, forKey: key)
```

```
[User Defaults] Attempt to set a non-property-list object
```

自作した構造体をUserDefaultsで扱う方法はいろいろあると思いますが、 `Data` 型に変換するとかんたんです。

まず、 `Data` 型に変換できるようにするため、自作した構造体を `Codable` プロトコルに準拠させます。

```swift:Monster.swift
extension Monster: Codable {}
```

追加する場合、 `Data` 型に変換してセットすればOKです。

```swift:自作した構造体をUserDefaultsに追加する
let monster = Monster(name: "uhooi", description: "ゆかいな　みどりの　せいぶつ。")

// `JSONEncoder` で `Data` 型へエンコードし、UserDefaultsに追加する
let jsonEncoder = JSONEncoder()
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
guard let data = try? jsonEncoder.encode(monster) else {
    return
}
UserDefaults.standard.set(data, forKey: key)
```

取得する場合は、UserDefaultsから `Data` 型として取得し、自作した構造体に変換し直せばOKです。

```swift:自作した構造体をUserDefaultsから取得する
// `JSONDecoder` で `Data` 型を自作した構造体へデコードする
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
guard let data = UserDefaults.standard.data(forKey: key),
    let monster = try? jsonDecoder.decode(Monster.self, from: data) else {
        return
}
```

これで自作した構造体などをUserDefaultsで扱えるようになりました！

## おまけ

### `synchronize()` は使うべきではない

以前は `UserDefaults.synchronize()` メソッドを呼び出さないと保存されないことがあったようです。
しかし、現在は「this method is unnecessary and shouldn't be used. （このメソッドは不要であり、使うべきではない）」とのことです。

### `value` は使わない

`NSObject` の拡張メソッドである `open func value(forKey key: String) -> Any?` と `open func setValue(_ value: Any?, forKey key: String)` のことです。

こちらは公式ドキュメントに「使うべきではない」と明確に書かれているかはわかりませんが、型が失われるので使うべきではないと考えます。

UserDefaultsへの保存時も取得時も、型を保ったまま処理しましょう。

```diff_swift
- UserDefaults.standard.setValue(data, forKey: key)
+ UserDefaults.standard.set(data, forKey: key)
```

```diff_swift
- UserDefaults.standard.value(forKey: key) as? Data
+ UserDefaults.standard.data(forKey: key)
```

## おわりに

iOSアプリではキーバリュー型のDBが非常に簡単に使えることがわかりました。

保存先をinfo.plistやRealmに変更しても動作するように、プロトコルを定義して使うとさらにいいです。
その際、プロトコルの名前は `TempStorageProtocol` のように、特定のDBに依存しない名前を付けると、保存先の変更時に呼び出し側を変更せずに済みます。
プロトコルを噛ませるとテストもしやすいです。

## 参考リンク

- [UserDefaults - Foundation | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/userdefaults)
- [synchronize() - UserDefaults | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize)
- [14 must knows for an iOS developer](https://swiftsailing.net/14-must-knows-for-an-ios-developer-5ae502d7d87f)
- [【iOS】UserDefaultsをSwiftらしく使う - Qiita](https://qiita.com/KokiEnomoto/items/c79c7f3793a244246fcf)
- [UserDefaultsでremoveAllできるようにする - Qiita](https://qiita.com/Tomooki_Tatsuguchi/items/04e353d2033589f1439c)