Manage View status with enum in Swift

This article is the 15th day article of Cluster Advent Calendar 2020. Yesterday was noir_neo's "Face Tracking with ARKit to move avatars right and left". If you want to display your avatar with Face Tracking, it will be very helpful ...!

Hello, this is the cluster, Inc. Hashimoto. I wrote Unity C # until the first half of this year, but recently I've been writing Swift exclusively. In cluster mobile app, I recently Native UI, but when I was developing it, I found it useful Swift enum. I will write about the state management of View.

Introduction

The native part of the cluster's mobile app uses the MVVM architecture (https://ja.wikipedia.org/wiki/Model_View_ViewModel) and uses RxSwift for data binding. So, the ViewModel has the state of View (ViewController in Swift).

Original code

Initially, I wrote a ViewModel like this and bound it with View. (* Private definitions are omitted because it will be redundant)

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(())
    }
}

What I was worried about in this code was that it was not associated with the data displayed in the list when it was in the idle state of HogeViewStatus (the state in which the list should be displayed). For example, while modifying the code, when HogeViewStatus is .empty, you can write code that hoges is not isEmpty, so depending on the screen specifications, it may be unexpected in View. It is expected that will be displayed.

Resolve using Associated Values

Swift has a mechanism called Associated Values that solves the above problems. (This is very convenient !!) It's a rough explanation, but Associated Values ​​is that you can assign a free type to each enum case. This time, [Hoge] can be added to idle.

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

Let's rewrite the ViewModel earlier using this.

ViewModel.swift


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

final class HogeViewModel {
    let updateViewStatus: Observable<HogeViewStatus>
    //It will be added to HogeViewStatus so it will not be necessary
    // 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 changed to be given to idle
                // self?.hogesRelay.accept(hoges)
                let status: HogeViewStatus = hoges.isEmpty
                    ? .empty
                    : .idle(hoges) //assocate hoges
                self?.hogeViewStatusRelay.accept(status)
            })
            .disposed(by: disposeBag)
    }

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

Now that we don't have to accept them separately, the actual state and array are no longer different. However, this alone is difficult to handle on the View side, so I will write an extension that extracts [Hoge] from HogeViewStatus.

HogeViewModel.swift


extension HogeViewModel {
    var hoges: [Hoge] {
        // (How to write this pattern match is also convenient, isn't it?)
        if case .idle(let hoges) = hogeViewStatusRelay.value {
            return hoges
        }
        return []
    }
}

Now you can safely handle states and arrays on the View side. Also, it seems that you do not have to write complicated code even if the state of View increases or you want to display data other than [Hoge]. The usage of Swift enum introduced this time is just an example, but I hope you can understand the convenience!

Tomorrow is YOSHIOKA_Ko57's "Change the UI judgment area for each platform in Unity". I'm looking forward to it ...!

Reference link

Recommended Posts

Manage View status with enum in Swift
Customize View with View Modifier in Swift UI
Display around fonts in Japanese with Swift: macOS
Transition to a view controller with Swift WebKit
Incorporate View created with Swift UI into ViewController
How to implement UICollectionView in Swift with code only
Quickly implement a singleton with an enum in Java
Compare objects in Swift
Division becomes 0 in Swift
Getting Started with Swift
Starting with Swift Swift UI
Review Swift enum types
Multidimensional array in Swift
Manage kube-bench execution results in cooperation with AWS Security Hub