Enum is All About Expressiveness

Not-that-hard choices, easy life.

January 11, 2020 -
#tech #ios #swift

Enum is one of my favorite gems empowered by Swift’s strong type system. As mentioned in The Elements of UI Engineering, making state consistent and predictable despite its entropy is a crucial part of building a delightful user experience. A properly designed data structure that could express “This is exactly what I mean. Period.” and “That absolutely can’t happen to me. Period.” explicitly will make the state management less painful.

Suppose we want to build the home screen of a stock trading app, where users’ investment portfolio is rendered. A simple model object that represents a portfolio holding could be something like this:

struct PortfolioHolding {
    let stockID: String
    let numberOfShares: Int
    // other properties
}

And portfolio home screen could be modeled as:

struct PortfolioHomeScreenModel {
    var portfolioHoldings: [PortfolioHolding]
}

Then view controller could render the view based on this model:

class PortfolioHomeScreenViewController: UIViewController {
    func apply(model: PortfolioHomeScreenModel) {
        // render view based on `model`
    }
}

Then at some point, the view controller gets confused - because there are multiple scenarios that could cause this portfolioHoldings to be empty, and we definitely want to render different contents for them:

  1. User puts all money in checking account, hasn’t made any investment yet - we want to render an onboarding state
  2. User has made some investments before but sold them all to purchase a house - we want to render an empty state
  3. User is still holding some stocks but runs out of 4G data … - we want to render a loading state

Our current PortfolioHomeScreenModel can’t really differentiate these contexts. Let’s refine it a bit with enum type:

struct InvestmentHomeScreenModel {
    var loadingState: LoadingState
    var investmentState: InvestmentState
    
    enum LoadingState {
        case loading
        case loaded
    }
    
    enum InvestmentState {
        case neverInvested
        case invested([InvestmentHolding])
    }
}

Much better! Now we could represent the 3 states as follow:

  1. Onboarding state: (loadingState: .loaded, investmentState: .neverInvested)
  2. Empty state: (loadingState: .loaded, investmentState: .invested([]))
  3. Loading state: (loadingState: .loading, investmentState: ???)

But…I bet you notice the problem, for loading state we’re not sure which value should be assigned to investmentState . Given both LoadingState and InvestmentState contain 2 cases, their combination contains 2 * 2 = 4 cases, but we only need 3.

Let’s eliminate that overhead:

struct PortfolioHomeScreenModel {
    var loadingState: LoadingState
        
    enum LoadingState {
        case loading
        case loaded(InvestmentState)

        enum InvestmentState {
            case neverInvested
            case invested([PortfolioHolding])
        }
    }
}

Then the 3 states could be represented as:

  1. Onboarding state: (loadingState: .loaded(.neverInvested))
  2. Empty state: (loadingState: .loaded(.invested([])))
  3. Loading state: (loadingState: .loading)

Then view controller could render the view based on an expressive and unambiguous model:

func apply(model: PortfolioHomeScreenModel) {
    switch model.loadingState {
        case .loading:
            // render loading state
        case let .loaded(investmentState):
            switch investmentState {
                case .neverInvested:
                    // render onboarding state
                case let .invested(portfolioHoldings):
                    if portfolioHoldings.isEmpty {
                        // render empty state
                    } else {
                        // render normal non-empty state
                    }
            }
    }
}

Swift’s own built-in API also embraces enum everywhere, some examples are :

  • Optional is an enum under the hood.
  • Result eliminates the impossible state that success and failure could be presented at the same time in a completion block like this.
  • fatalError actually has a return value called Never, which is an empty enum.

Not-that-hard choices, but easy life! With all that, a user with a well diversified and high ROI portfolio is less likely to wake up to a home screen being told “Hey, let’s make your first investment”, freak out, uninstall the app, and give a 1 star on App Store.

Happy enuming!