この記事はクラスター 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な定義等は省略しています)
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という仕組みが用意されています。(これがめっちゃ便利!!)
雑な説明ですが、Associated Valuesはenumのcase毎に自由な型を付与することができるというものです。今回はidle
に[Hoge]
を付与できるようにしています。
enum HogeViewStatus {
case processing
case empty
case idle([Hoge])
}
これを使って、先程のViewModelを書き直してみます。
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を書いてあげます。
extension HogeViewModel {
var hoges: [Hoge] {
// (このパターンマッチの書き方も便利ですよね)
if case .idle(let hoges) = hogeViewStatusRelay.value {
return hoges
}
return []
}
}
以上で状態と配列をView側で安全に扱えるようになりました。また、Viewの状態が増えたり、[Hoge]
以外のデータを表示したくなっても複雑なコードを書かなくて済みそうですね。今回紹介したSwiftのenumの使い方はほんの一例ですが、便利さが伝わっていたら幸いです!
明日は YOSHIOKA_Ko57さんの「UnityでプラットフォームごとにUIの判定エリアを変える」 です。楽しみですね...!
#参考リンク