SwiftでViewの状態をenumで管理する


この記事は[クラスター Advent Calendar 2020](https://qiita.com/advent-calendar/2020/cluster) 15日目の記事です。
昨日は noir_neoさんの「[ARKit で Face Tracking して左右を正しくアバターを動かす](https://qiita.com/noir_neo/items/9c39d85ca1fb2fd3dc68)」でした。Face Trackingでアバターを表示させたくなったらとても参考になりそうです...!

こんにちは、クラスター社の橋本です。今年の前半まではUnityのC#を書いてましたが、最近では専らSwiftを書いています。
[clusterのモバイルアプリ](https://apps.apple.com/app/id1490075175?l=ja)では最近[UIをネイティブ化](https://qiita.com/mizoguche/items/09719bb1ea3c3265dcdc)したんですが、それを開発しているときに[Swiftのenum](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html)便利だな〜と感じたことをViewの状態管理を題材に書いていきます。

#はじめに
clusterのモバイルアプリのネイティブ部分では、[MVVMアーキテクチャ](https://ja.wikipedia.org/wiki/Model_View_ViewModel)を採用していて、[RxSwift](https://github.com/ReactiveX/RxSwift) を使ったデータバインディングを行っています。
なので、View(Swiftで言うところのViewController)の状態はViewModelが持っています。

#当初のコード
当初はこんな感じのViewModelを書いて、ViewでBindしていました。(※冗長になるのでprivateな定義等は省略しています)

```ViewModel.swift
enum HogeViewStatus {
    case processing
    case empty
    case idle
}

final class HogeViewModel {
    let updateViewStatus: Observable<HogeViewStatus>
    var hoges: [Hoge] {
        return hogesRelay.value
    }

    init(hogeRepository: HogeRepository) {
        self.hogeRepository = hogeRepository

        updateViewStatus = hogeViewStatusRelay.asObservable()

        refreshRelay
            .flatMapLatest { _ in hogeRepository.get() }
            .subscribe(onNext: { [weak self] hoges in
                self?.hogesRelay.accept(hoges)
                let status: HogeViewStatus = hoges.isEmpty
                    ? .empty
                    : .idle
                self?.hogeViewStatusRelay.accept(status)
            })
            .disposed(by: disposeBag)
    }

    func refresh() {
        refreshRelay.accept(())
    }
}

```

このコードで気になっていたことは、`HogeViewStatus`の`idle`状態（リストが表示されているべき状態）のときにリストで表示されるデータと紐付いていないことでした。例えばですが、コードを改修していく中で`HogeViewStatus`が`.empty`のときに`hoges`が`isEmpty`じゃないみたいなコードを書きうるので、画面の仕様によってはViewで予期しないものを表示してしまうみたいなことが予想されます。

# Associated Valuesを使って解決する
Swiftには上記のような問題を解決してくれる[Associated Values](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html)という仕組みが用意されています。（これがめっちゃ便利！！）
雑な説明ですが、Associated Valuesはenumのcase毎に自由な型を付与することができるというものです。今回は`idle`に`[Hoge]`を付与できるようにしています。

```
enum HogeViewStatus {
    case processing
    case empty
    case idle([Hoge])
}
```

これを使って、先程のViewModelを書き直してみます。

```ViewModel.swift
enum HogeViewStatus {
    case processing
    case empty
    case idle([Hoge])
}

final class HogeViewModel {
    let updateViewStatus: Observable<HogeViewStatus>
    // HogeViewStatusに付与されるようになるので不要になる
    // var hoges: [Hoge] {
    //     return hogesRelay.value
    // }

    init(hogeRepository: HogeRepository) {
        self.hogeRepository = hogeRepository

        updateViewStatus = hogeViewStatusRelay.asObservable()

        refreshRelay
            .flatMapLatest { _ in hogeRepository.get() }
            .subscribe(onNext: { [weak self] hoges in
                // hogesはidleに付与するように変更
                // self?.hogesRelay.accept(hoges)
                let status: HogeViewStatus = hoges.isEmpty
                    ? .empty
                    : .idle(hoges) // hogesをassocate
                self?.hogeViewStatusRelay.accept(status)
            })
            .disposed(by: disposeBag)
    }

    func refresh() {
        refreshRelay.accept(())
    }
}
```
これで別々にacceptしなくて良くなったので、状態と配列の実態が異なることもなくなるようになりました。
ただこれだけだとView側で扱いづらいので、`HogeViewStatus`から`[Hoge]`を取り出すextensionを書いてあげます。

```HogeViewModel.swift
extension HogeViewModel {
    var hoges: [Hoge] {
        // (このパターンマッチの書き方も便利ですよね)
        if case .idle(let hoges) = hogeViewStatusRelay.value {
            return hoges
        }
        return []
    }
}
```

以上で状態と配列をView側で安全に扱えるようになりました。また、Viewの状態が増えたり、`[Hoge]`以外のデータを表示したくなっても複雑なコードを書かなくて済みそうですね。今回紹介したSwiftのenumの使い方はほんの一例ですが、便利さが伝わっていたら幸いです！

明日は YOSHIOKA_Ko57さんの「UnityでプラットフォームごとにUIの判定エリアを変える」 です。楽しみですね...!

#参考リンク
- https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html