try! Swiftの復習: 型消去を利用したライブラリを作ってみた

try! Swift では [Gwendolyn Weston](https://twitter.com/purpleyay)さんによる`Keep Calm and Type Erase On`で紹介されていた`Type Erasure（型消去）`がとても印象に残ったのですが、セッションを聞いただけでは使いどころがピンときませんでした。
（ポケモンを知らない自分のせいかもしれないですが😅）

型消去の復習を兼ねて、NSUserDefaultsを題材にして `TypeDefaults` というライブラリを作ってみたので、その紹介です。

<blockquote>
  <p><a rel="noreferrer" target="https://github.com/tasanobu/TypedDefaults/" href="https://github.com/tasanobu/TypedDefaults/">tasanobu/TypedDefaults</a>
  </p>
  <p>
  TypedDefaults is a utility library to type-safely use NSUserDtefaults.
  </p>
</blockquote>

# TypeDefaultsの特徴
- `NSUserDefaults`でカスタム型をType Safeに扱える
- `Dependency Injection` 対応

## NSUserDefaultsでカスタム型をType Safeに扱える
カスタム型をType SafeにNSUserDefaultsに保存できます。
カスタム型は後述する`DefaultConvertible`プロコトルに準拠していればよく、NSObjectを継承する必要はありません。
そのため、カスタム型には `SwiftのネイティブClass`, `Struct`, `Enum` が利用可能です。（もちろん、NSObjectのサブクラスも可）

### `DefaultConvertible`プロトコル
```
public protocol DefaultConvertible {

    static var key: String { get }

    init?(_ object: AnyObject)

    func serialize() -> AnyObject
}
```

カスタム型は、`NSUserDefaults`に`AnyObject`で保存します。<br/>

- 保存時: `serialize()`
- 取得時に`init?(_ object:)`

が呼び出されます。

また、カスタム型はアプリで利用する個々の設定に対応することを前提にしています。
カスタム型とキーを1:1に対応付けするため、型プロパティとして`key`を用意しています。

### カスタム型の例
ここでは、カメラ設定として `カメラロールへの保存フラグ` `保存サイズ`を持つ型を例にしています。

```swift
/// カメラ設定として `カメラロールへの保存フラグ` `保存サイズ`を持つ型
struct CameraConfig: DefaultConvertible {
    enum Size: Int {
        case Large, Medium, Small
    }

    var saveToCameraRoll: Bool
    var size: Size

    // MARK: DefaultConvertible

    static let key = "CameraConfig"

    init?(_ object: AnyObject) {
        guard let dict = object as? [String: AnyObject] else { return nil }

        self.saveToCameraRoll = dict["cameraRoll"] as? Bool ?? true
        if let rawSize = dict["size"] as? Int,
         let size = Size(rawValue: rawSize) {
            self.size = size
         } else {
            self.size = .Medium
        }
    }

    func serialize() -> AnyObject {
        return ["cameraRoll": saveToCameraRoll, "size": size.rawValue]
    }
}
```

### 利用方法
カスタム型をNSUserDefaultsへ保存するためには、NSUserDefaultsへのアクセスを中継する型である`PersistentStore`を利用します。
利用方法は次の通りです。

```swift
/// 利用するカスタム型をインスタンス生成時に指定します。
let userDefaults = PersistentStore<CameraConfig>()

// CameraConfigをインスタンス化
var cs = CameraConfig([:])! 

// set
userDefaults.set(cs)
// get
userDefaults.get()?.size // Medium

/// カスタム型のサイズを変更
cs.size = .Large

// set
userDefaults.set(cs)
// get
userDefaults.get()?.size // Large
```

## Dependency Injection 対応
`NSUserDefaults`はデータを永続化するため、Unit Testと相性がよくありません。
`NSuserDefaults`に保存したカスタム型の値によって挙動が変わる型をテストするために、DIに使える型 `InMemoryStore` `AnyStore`  を用意しています。

`InMemoryStore`は `PersistentStore`と同じ`DefaultStoreType`に準拠しており、インターフェースは基本同じです。
`InMemoryStore`はセットされた値をメモリ上で保持し永続化しない点が`PersistentStore`と異なります。

一方、`AnyStore`は `InMemoryStore`と`PersistentStore` かを抽象化する型です。
ここで`Type Erasure`を使ってます。

### サンプル
テスト時に`InMemoryStore` `AnyStore` を使う例です。

次のような`CameraViewController`というUIViewControllerを継承したクラスがあったとします。
NSUserDefaultsに保存したカスタム型を保持するプロパティ`config`をDI可能にするため`AnyStore`型にします。

```swift
/// アプリのコード
class CameraViewController: UIViewController {
    lazy var config: AnyStore<CameraConfig> = {
        let ds = PersistentStore<CameraConfig>()
        return AnyStore(ds)
    }()

    ...
}
```


プロパティ`config`の型を`PersistentStore`ではなく`AnyStore`にしたことで、テスト時は次のように`InMemoryStore`に差し替えることが可能となります。

```swift
/// テストコード
class CameraViewControllerTests: XCTestCase {
    var viewController: CameraViewController!

    override func setUp() {
        viewController = CameraViewController()

        let defaultConfig = CameraConfig([:])!
        let ds = InMemoryStore<CameraConfig>()
        ds.set(defaultConfig) //
        viewController.config = AnyStore(ds)
    }
}
```

## まとめ
NSUserDefaultsをユースケースとして、Type Erasureを復習してみました。
Type Erasure自体は抽象的な手法だと思うのですが、意外にアプリ開発の現場で結構使いどころがありそうですね。

冒頭でも書きましたが、この記事で使ったコードはライブラリ化してGithubに公開しています。
もしよかったら、使ってみて下さい。
改善の余地は色々あると思うので、PRやIssueでフィードバック頂けると幸いです。

<blockquote>
  <p><a rel="noreferrer" target="https://github.com/tasanobu/TypedDefaults/" href="https://github.com/tasanobu/TypedDefaults/">tasanobu/TypedDefaults</a>
  </p>
  <p>
  TypedDefaults is a utility library to type-safely use NSUserDefaults.
  </p>
</blockquote>