Five powerful, yet lesser-known ways to use Swift enums
Basics article available: EnumsSwift’s implementation of enums is arguably one of the most interesting aspects of the language as a whole. From defining finite lists of type-safe values, to how associated values can be used to express model variants, and beyond — there’s a wide range of situations in which Swift enums can be used in really powerful ways.
This week, let’s focus on that “and beyond” part, by taking a look at a few somewhat lesser-known ways in which enums can be used to solve various problems in Swift-based apps and libraries.
Namespaces and non-initializable types
A namespace is a programming construct that enables various types and symbols to be grouped together under one name. While Swift doesn’t ship with a dedicated namespace
keyword, like some other languages do, we can use enums to achieve a very similar result — in that we can create a hierarchy of nested types to add some additional structure in certain situations.
For example, Apple’s Combine framework uses this technique to group its many Publisher
types into one Publishers
namespace, which is simply declared as a case-less enum that looks like this:
enum Publishers {}
Then, each Publisher
type is added by extending the Publishers
“namespace”, for example like this:
extension Publishers {
struct First<Upstream>: Publisher where Upstream: Publisher {
...
}
}
Using the above kind of namespacing can be a great way to add clear semantics to a group of types without having to manually attach a given prefix or suffix to each type’s name.
So while the above First
type could instead have been named FirstPublisher
and placed within the global scope, the current implementation makes it publicly available as Publishers.First
— which both reads really nicely, and also gives us a hint that First
is just one of many publishers available within the Publishers
namespace. It also lets us type Publishers.
within Xcode to see a list of all available publisher variations as autocomplete suggestions.
What makes enums particularly useful within this context is that they can’t be initialized, which sends another strong signal that the types that we end up using as namespaces aren’t meant to be used on their own.
Along those same lines, that non-initializable characteristic also makes enums a great option when implementing phantom types — which are types that are used as markers, rather than being instantiated to represent values or objects. The same thing goes for types that only contain static APIs, such as the following AppConfig
type, which contains various static configuration properties for an app:
enum AppConfig {
static let apiBaseURL = URL(string: "https://api.swiftbysundell.com")!
static var enableExperimentalFeatures = false
...
}
Iterating over cases
Next, let’s take a look at how enums can be iterated over using the built-in CaseIterable
protocol. While we’ve already explored enum iterations before, let’s take a closer look at how we could use those capabilities when designing reusable abstractions and libraries.
For example, the Publish static site generator that powers this website uses a Website
protocol to enable each site to freely configure what sections that it contains, and requires the type used to define those sections to conform to CaseIterable
:
protocol Website {
associatedtype SectionID: CaseIterable
...
}
The above is a simplified version of the actual Website
protocol, which you can find on GitHub here.
That design in turn enables the library to iterate over each section in order to generate its HTML — without requiring users to manually supply any form of array of other collection to iterate over, since using CaseIterable
automatically makes an allCases
collection available on all conforming types:
extension Website {
func generate() throws {
try SectionID.allCases.forEach(generateSection)
}
private func generateSection(_ section: SectionID) throws {
...
}
}
Again, the above is a simplification of the actual implementation of Publish, but the core pattern is still the same.
At the call site, a given website can then declare what sections that it contains simply by defining a nested SectionID
enum that conforms to CaseIterable
, like this:
struct MyPortfolio: Website {
enum SectionID: CaseIterable {
case apps
case blog
case about
}
...
}
Custom raw types
That an enum can be backed by a built-in raw type, such as String
or Int
, is definitely a well-known and commonly used feature, but the same thing can’t be said for custom raw types — which can be really useful in certain situations.
For example, let’s say that we’re working on a content management app, and that we enable each entry within our app to be tagged with a series of tags. Since we’d like to add a bit of extra type safety to our code, we don’t store our tags as plain strings, but rather using a Tag
type that wraps an underlying String
value. To make that type as easy to use as possible, we still enable it to be expressed using a string literal, just like a raw string would, giving us the following implementation:
struct Tag: Equatable {
var string: String
}
extension Tag: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
string = value
}
}
Now let’s say that we’d like to define a pre-determined set of higher-level categories that are each backed by a given Tag
, to enable us to provide a more streamlined filtering UI within our app. If we were to do that using an enum, it might initially seem like we’d need to revert back to using raw strings for the underlying tags that back each category — but that’s actually not the case.
Since our Tag
type can be expressed using literals, and since it conforms to Equatable
, we can actually declare our Category
enum with our own Tag
type as its backing raw type:
enum Category: Tag {
case articles = "article"
case videos = "video"
case recommended
...
}
Note how we can choose whether to customize the underlying raw value for each case, or just use the ones that the compiler will synthesize for us — just like we can when using raw strings, or any other built-in type. Pretty cool!
Convenience cases
When designing any kind of API, it’s important to try to strike a nice balance between the structure and consistency of the underlying implementation, as well as making the call sites as clear and simple as possible. To that end, let’s say that we’re working on an animation system, and that we’ve defined a RepeatMode
enum that enables easy customization of how many times that an animation should repeat before being removed.
Similar to the Tag
type from before, we could’ve just used a simple Int
to represent that sort of repeat count, but we’ve opted for the following API in order to make it easy to express common repeat modes, such as once
and never
:
extension Animation {
enum RepeatMode: Equatable {
case once
case times(Int)
case never
case forever
}
}
However, while the above results in a very neat API (by enabling us to use “dot-syntax”, like .times(5)
and .forever
), our internal handling code ends up becoming somewhat complex — since we’ll need to handle each RepeatMode
case separately, even though all of that logic is really similar:
func animationDidFinish(_ animation: Animation) {
switch animation.repeatMode {
case .once:
if animation.playCount == 1 {
startAnimation(animation)
}
case .times(let times):
if animation.playCount <= times {
startAnimation(animation)
}
case .never:
break
case .forever:
startAnimation(animation)
}
}
While there are a number of different approaches that we could take in order to solve the above problem (including using a more free-form struct, rather than an enum), let’s keep our enum-based API for now, while also simplifying our internal implementation and handling code.
To do that, let’s reduce our number of actual cases to two — one that lets us specify a numeric repeat count, and one that tells our system to repeat a given animation forever:
extension Animation {
enum RepeatMode: Equatable {
case times(Int)
case forever
}
}
Then, to keep maintaining the exact same public API as we had before, let’s augment the new version of our enum with two static properties — one for each of the two cases that we just removed:
extension Animation.RepeatMode {
static var once: Self { .times(1) }
static var never: Self { .times(0) }
}
A really cool thing about the above change is that it doesn’t actually require us to change our code at all. Our previous switch
statement will keep working just as before (thanks to Swift’s advanced pattern matching capabilities), and our public API remains identical.
However, since we now only have two actual cases to deal with, we can heavily simplify our code once we’re ready to do so — for example like this:
func animationDidFinish(_ animation: Animation) {
switch animation.repeatMode {
case .times(let times):
if animation.playCount <= times {
startAnimation(animation)
}
case .forever:
startAnimation(animation)
}
}
The reason we still keep a separate case for forever
(rather than using .times(.max)
) is that we might want to handle that case somewhat differently — for example in order to perform certain optimizations for animations that we know will never be removed, and to accurately represent an infinite repeat count (because Int.max
is technically not forever).
Cases as functions
Finally, let’s take a look at how enum cases relate to functions, and how Swift 5.3 brings a new feature that lets us combine protocols with enums in brand new ways.
As an example, let’s say that we’re currently using the following enum to keep track of the loading state of a given Product
model, with associated values for any loaded instance, as well for any Error
that was encountered:
enum ProductLoadingState {
case notLoaded
case loading
case loaded(Product)
case failed(Error)
}
A really interesting aspect of enum cases with associated values is that they can actually be used directly as functions. For example, here we’re enabling a ProductViewModel
to be initialized with an optional preloaded Product
value, which we then map directly into our .loaded
case as if that case was a function:
class ProductViewModel: ObservableObject {
@Published private(set) var state: ProductLoadingState
...
init(product: Product?) {
state = product.map(ProductLoadingState.loaded) ?? .notLoaded
}
...
}
That feature comes particularly in handy when chaining multiple expressions together, for example when using Combine. Here is a more advanced version of the above ProductViewModel
example, which uses the Combine-powered URLSession
API to load a product over the network, and then maps the result to a ProductLoadingState
value, just like above:
class ProductViewModel: ObservableObject {
@Published private(set) var state = ProductLoadingState.notLoaded
private let id: Product.ID
private let urlSession: URLSession
...
func load() {
state = .loading
urlSession
.dataTaskPublisher(for: .product(withID: id))
.map(\.data)
.decode(type: Product.self, decoder: JSONDecoder())
.map(ProductLoadingState.loaded)
.catch({ Just(.failed($0)) })
.receive(on: DispatchQueue.main)
.assign(to: &$state)
}
}
Swift 5.3 introduces yet another capability that further brings functions and enums closer together, by allowing enum cases to fulfill static protocol requirements. Let’s say that we wanted to generalize the concept of preloading across our app — by introducing a generic Preloadable
protocol that can be used to create an already loaded instance of a given type, like this:
protocol Preloadable {
associatedtype Resource
static func loaded(_ resource: Resource) -> Self
}
What’s really nice is that in order to make our ProductLoadingState
enum conform to the above protocol, we just have to declare what type of Resource
that it’s loading, and our loaded
case will be used to satisfy our protocol’s function requirement:
extension ProductLoadingState: Preloadable {
typealias Resource = Product
}
Conclusion
Depending on who you ask, enums are either under-utilized, or over-used in Swift. Personally, I think it’s fantastic that Swift’s enums offer such a wide range of features that can be used in so many different situations, but that doesn’t mean that they’re a “silver bullet” that we should always reach for.
It’s important to remember that enums work best when modeling finite lists, and that other language features — including structs and protocols — might be a better fit when we need to build something a bit more flexible or dynamic. But, at the end of the day, the better our knowledge of Swift’s various language features, the better choices we’ll be able to make within each given situation.
Got questions, comments, or feedback? Feel free to email me, or send me a tweet @johnsundell.
Thanks for reading! 🚀