An Ode to Swift Enums: The View Models That Could

This post is brought to you by Emerge Tools, the best way to build on mobile.
Swift enums are the swiss army knife of the iOS developer’s tool belt. Adept at solving many problems in several novel ways, its utility extends far beyond what we might’ve come to expect from an enumeration (which rings louder for those of who started with Objective-C). As such, a lovely way to embrace your fellow Swift enum is a lightweight view model.
Consider:
- A baskeball practice planner app (hey, I know of one!)
- Which shows a list of practices
- …and each with a practice type: team practice, workout or skills training.
That’s an ideal time to flex enum’s raw might:
enum PracticeType {
case teamPractice, workout, skillsTraining
}
Text Me
A fine start, though you’ll likely need to show the actual type as a String in your interface. Being the epitome of flexibility, there are two obvious routes fit to the task when using an enum:
Option 1: Use a String as its rawValue
:
enum PracticeType: String {
case teamPractice, workout, skillsTraining
}
Here, PracticeType
has a raw value type of String
:
let team = PracticeType.teamPractice
// teamPractice
print(team)
Serviceable in many cases. We’ll come back to this. For now, there’s also…
Option 2: Adopt CustomStringConvertible
:
enum PracticeType: CustomStringConvertible {
case teamPractice, workout, skillsTraining
var description: String {
switch self {
case .teamPractice:
"Team Practice"
case .workout:
"Workout"
case .skillsTraining:
"Skill Training"
}
}
}
CustomStringConvertible
is a lightweight protocol which allows you to describe the adopting type a String
via its description
property:
let team = PracticeType.teamPractice
// Team Practice
print(team.description)
let concatenatedThoughts = """
However, any Swift type can be represented as a `String`, it's a nicety built into the language. Using the `String(describing:)` initializer will still yield a `String` of the passed in type. In this case, if the type adopts this protocol, then Swift defers to the `description` implementation.
"""
If you want to be an academic, you could make the argument (perhaps, too easily) that this is a misuse of the protocol. Here’s Apple:
Accessing a type’s description property directly or using
CustomStringConvertible
as a generic constraint is discouraged.
To wit, in their example — they interpolate a String
to represent a type described in a more holistic sense, rather than a single case of an enum:
struct Point {
let x: Int, y: Int
}
extension Point: CustomStringConvertible {
var description: String {
return "(\(x), \(y))"
}
}
Which is appropriate for you is a matter for you to decide, but what I can tell you is I’ve adorned enums the world over with CustomStringConvertible
and I’ve been no worse off.
Identity Crisis
Moving along, now — what if we need to show such a type in a Picker
or ForEach
? In these situations, the concept of identity is crucial. How do we individualize each type — shouldn’t enums inherently be described in such a fashion by design? In SwiftUI, we solve this problem (in part) via Identifiable
conformance. And in enums? It’s quite trivial:
enum PracticeType: CustomStringConvertible, Identifiable {
case teamPractice, workout, skillsTraining
var description: String {
switch self {
case .teamPractice:
"Team Practice"
case .workout:
"Workout"
case .skillsTraining:
"Skill Training"
}
}
var id: Self { self }
}
Now, you’re free to show it in several different SwiftUI views — thanks to each type representing its own unique value in a way SwiftUI understands:
@State private var practiceType: PracticeType? = nil
Picker("", selection: $practiceType) {
// Your picker representation
}
This view, here — a Picker
, naturally leads us to another issue — how do we show all of the cases? Certainly, something like this is tiresome…
Picker("", selection: $practiceType) {
Text(PracticeType.teamPractice)
.tag(PracticeType.teamPractice)
Text(PracticeType.workout)
.tag(PracticeType.workout)
Text(PracticeType.skillsTraining)
.tag(PracticeType.skillsTraining)
}
…and indeed, unnecessary thanks to more of what Swift enums offer us.
Iteration
Moving on, we now arrive at CaseIterable
, yet another protocol the Swift compiler can handle for us. This protocol allows us to represent our type as a collection, accessed via its allCases
property. Though common in enums without an associated types, it can be in enums with associated types all the same. In our scenario, though — all that’s required is to simply declare conformance:
enum PracticeType: CustomStringConvertible, Identifiable, CaseIterable {
// No other changes required from us
}
Now, the ergonomics of utilizing a Picker
with our enum becomes much more tolerable (and scalable):
Picker("", selection: $practiceType) {
ForEach(PracticeType.allCases) { practice in
Text(practice.description)
.tag(practice)
}
}
For the Love of the Game
Enums are a quintessential fit in the SwiftUI ecosystem, lending itself well to many of its design choices. Beyond using them in View
types, I find myself having maybe a little too much fun with them. I could write a book (technically, because I’m a glutton for punishment, I wrote five) over the flexibility of the Swift enum. I can think of no better way to wrap this post up other than showing them off like a show pony.
For example, could I interest you in a random practice type?
let randomPractice: PracticeType = PracticeType.allCases.randomElement() ?? .teamPractice
Create extensions for further use cases:
Or, as your app grows and feature emerge, I also find myself sticking several quality of life extensions on my enums:
extension PracticeType {
var subTitle: String {
switch self {
case .teamPractice:
"Practices suited towards a team environment with five or more players."
case .workout:
"An individual workout."
case .skillsTraining:
"Skills-based training sessions, where drills are the primary activity.""
}
}
Don’t get me started on the flexibility of associated values:
Associated values make what was already a powerful little construct stray into OP territory. Consider loading states:
enum PracticeState {
case notStarted
case inProgress(TimeInterval)
case completed(totalDuration: Int)
}
struct PracticeView: View {
@State private var state: PracticeState = .notStarted
@State private var elapsedTime: TimeInterval = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 20) {
switch state {
case .notStarted:
Button("Start Practice") {
state = .inProgress(0)
}
case .inProgress(let timeElapsed):
Text("Time Elapsed: \(Int(timeElapsed))s")
Button("End Practice") {
state = .completed(totalDuration: Int(timeElapsed))
}
.padding(.top)
case .completed(let totalDuration):
Text("Practice Completed")
Text("Total Duration: \(totalDuration) seconds")
Button("Restart") {
state = .notStarted
}
.padding(.top)
}
}
.onReceive(timer) { _ in
if case .inProgress(let timeElapsed) = state {
state = .inProgress(timeElapsed + 1)
}
}
.padding()
}
}
Mocks and Dependency Injection:
Continuing on with associated values, they naturally slot into SwiftUI previews and mock services:
enum Environment {
case production(Service)
case staging(Service)
case mock
var analyticsService: AnalyticsService {
switch self {
case .production(let service), .staging(let service):
return service
case .mock:
return Service.mocked()
}
}
}
let env: Environment = .production(ProdAnalytics())
let analyticsSerivce = env.analyticsService
Stay vanilla:
All this talk of creative use cases — it’s possible I’ve overlooked using enums just as they were originally envisioned. Of course, they are practical for that, too. But because Swift treats them as more of first class citizen and not a singular value to switch on, they are suited to many different tasks:
enum FeatureFlags {
case onboardingV2
case usePracticePlanner
case environmentValue(value: String)
var isEnabled: Bool {
switch self {
case .onboardingV2:
return true
case .usePracticePlanner:
return false
case .environmentValue(let val):
return ProcessInfo.processInfo.environment[val] == "true"
}
}
}
// Later on...
if FeatureFlags.onboardingV2.isEnabled {
NewOnboardingView()
} else {
OnboardingView()
}
Wrapping Up
In practice, many of my enums become tiny little view models. Just because they can do so much — should they? Personally, I have never been one to get lost in the weeds on questions like that in my career. I simply enjoy the flexibility that the Swift language provides, and I deploy these tiny little powerhouses to make life easier and my apps ship faster.
Enums represent the Ne plus ultra of Swift’s type system — what was historically a tiny little construct relegated to simple multiple case representations has instead evolved into a durable, flexible type capable of handling just about any situation that both Swift and SwiftUI could demand of it. As such, the evolution has been something along the lines of using a lightweight NSObject
, which became a lightweight Struct
, and then become a lightweight Enum
. What a time to be alive 🍻.
Until next time ✌️