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:
- User puts all money in checking account, hasn’t made any investment yet - we want to render an onboarding state
- User has made some investments before but sold them all to purchase a house - we want to render an empty state
- 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:
- Onboarding state:
(loadingState: .loaded, investmentState: .neverInvested)
- Empty state:
(loadingState: .loaded, investmentState: .invested([]))
- 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:
- Onboarding state:
(loadingState: .loaded(.neverInvested))
- Empty state:
(loadingState: .loaded(.invested([])))
- 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 calledNever
, 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!