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


この記事はクラスター Advent Calendar 2020 15日目の記事です。
昨日は noir_neoさんの「ARKit で Face Tracking して左右を正しくアバターを動かす」でした。Face Trackingでアバターを表示させたくなったらとても参考になりそうです...!

こんにちは、クラスター社の橋本です。今年の前半まではUnityのC#を書いてましたが、最近では専らSwiftを書いています。
clusterのモバイルアプリでは最近UIをネイティブ化したんですが、それを開発しているときにSwiftのenum便利だな〜と感じたことをViewの状態管理を題材に書いていきます。

はじめに

clusterのモバイルアプリのネイティブ部分では、MVVMアーキテクチャを採用していて、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(())
    }
}

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

Associated Valuesを使って解決する

Swiftには上記のような問題を解決してくれるAssociated Valuesという仕組みが用意されています。(これがめっちゃ便利!!)
雑な説明ですが、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の判定エリアを変える」 です。楽しみですね...!

参考リンク