LoginSignup
4
1

More than 3 years have passed since last update.

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

Posted at

この記事はクラスター 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の判定エリアを変える」 です。楽しみですね...!

参考リンク

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1