Swift 4のCodable x UserDefaults


VASILYのiOSエンジニアにこらすです。

今回のテックブログではiOS・macOS・watchOS・tvOSのUserDefaultsにユーザー設定などを保存するのに便利なラッパーライブラリ [Default](https://github.com/Nirma/Default/blob/master/README.md) を作ったので紹介します。

https://github.com/Nirma/Default


# Defaultとは？
Defaultは、Codableに準拠するカスタムオブジェクトを保存するための拡張機能を提供するライブラリです。プロトコル`DefaultStorable`を介して、UserDefaultsに以下で説明する新しいインタフェースを提供することで、UserDefaultsを拡張します。 Codableサポート拡張機能とDefaultStorableプロトコル拡張機能いずれかを使うこともできますし、両方つかうこともできます。



<!-- more -->



# Defaultを使うメリットは？
UserDefaultsには、保存したキーをtypoしたり、読み書きしている場所を探すのが難しかったりといった難点があります。
ですが、このDefaultを使って保存されるオブジェクト型を定義すると、`DefaultStorable` に準拠する型をプロジェクト内で検索することで保存されるデータを追うのが簡単になります。 `UserDefaults` に格納する専用のオブジェクトを定義すれば、特定のデータを論理的にグループ化することができます。

# NSCoding 時代の実装
Swiftでカスタムオブジェクトを宣言した後は、 `NSCoding`に準拠し、` NSObject`から継承し、適切なEncode / Decodeメソッドを実装すれば、 `UserDefaults`に直接保存することができるようになります。
これを自前で対応しようとすると、以下のような煩雑なコードが必要になります。

### クラスを定義し、 `NSCoding`に準拠し、必要なDecode / Encodeメソッドを実装する

```swift
class VolumeSetting: NSObject, NSCoding {
    let sourceName: String
    let value: Double
    init(sourceName: String, value: Double) {
        self.sourceName = sourceName
        self.value = value
    }
    required init(coder decoder: NSCoder) {
        self.sourceName = decoder.decodeObject(forKey: "sourceName") as? String ?? ""
        self.value = decoder.decodeDouble(forKey: "value")
    }

    func encode(with coder: NSCoder) {
        coder.encode(sourceName, forKey: "sourceName")
        coder.encode(value, forKey: "value")
    }
}
```

### オブジェクトを作成し、 `NSKeyedArchiver`を使用してインスタンスを` Data`にアーカイブして保存する

```swift
let setting = VolumeSetting(sourceName: "Super Expensive Headphone Amp", value: 0.4)
let encodedData = NSKeyedArchiver.archivedData(withRootObject: setting)
UserDefaults.standard.set(encodedData, forKey: "volume")
```

### 読み出すときは `NSKeyedUnarchiver` を使う

``` swift
if let data = UserDefaults.standard.data(forKey: "volume"),
   let volumeSetting = NSKeyedUnarchiver.unarchiveObject(with: data) as? VolumeSetting {
   // do something
}
```

# Defaultを使った実装

一方、 Default を使えば、以下のようなシンプルなコードで実現できます。

### 保存対象のオブジェクトを定義する

`DefaultStorable` プロトコルに準拠したstructを定義します。

```swift
struct VisualSettings: Codable, DefaultStorable {
    let themeName: String
    let backgroundImageURL: URL?
}
```

### 保存処理

```swift
let settings = VisualSettings(themeName: "bright", backgroundImageURL: URL(string: "https://..."))
settings.storeToDefaults()
```

### 読み込み

```swift
if let settings = VisualSettings.fetchFromDefaults() {
    // Do something
}
```

# もう一つのメリット
このアプローチのもう一つの利点は、すべてのオブジェクトを一つのファイルに定義することで、`UserDefaults` に格納されるものを非常に簡単に見ることができることです。

# Defaultの設計について
`UserDefaults`にカスタムオブジェクトを保存するためには、そのオブジェクトは`NSCoding`に準拠する必要があります。
`NSCoding`に準拠するにはDecoding / Encodingメソッドを実装する必要があり、少し手間がかかります。
一方、嬉しいことにSwiftの`Data`型が`NSCoding`に準拠しています。オブジェクトを`Data`に変換する方法を見つけることができれば、それを`UserDefaults`に格納することができます。
Swift 4から追加された[Codable](https://developer.apple.com/documentation/swift/codable)プロトコルは簡単に`Data`に変換する事ができます。
Swift 4以降のプロジェクトであれば、`Codable`に準拠したモデルオブジェクトを作ることが多くなると思います。このライブラリは、そういう`Codable`プロトコルに準拠したオブジェクトを`UserDeafaults`に読み書きすることができるようになっています。

結構シンプルですね！

# まとめ
Default は、非常に軽くてシンプルなカスタムオブジェクトを扱う `UserDefaults`のラッパーです。
GitHubに公開してあるので、[Default](https://github.com/Nirma/Default) をインストールして遊んでみたい場合は、Carthage か CocoaPods を使って試してみてください。

VASILYでは、OSSなどSwift 4での開発に興味があるエンジニアを募集しています。ぜひオフィスに遊びに来てください。

<iframe frameborder='0' height='305px' name='wantedly_project_widget_88978' scrolling='no' src='https://www.wantedly.com/projects/88978/widget' style='border: none; max-width: 100%; min-width: 240px; width: 540px;'></iframe>


ー　にこらす 👍