Swift — Enums

Jullian Mercier
6 min readJul 21, 2019

--

Enums are the representation of different — yet related — scenarios within a specific context.

They define a common type for a group of related values and enables you to work with those values in a type-safe way within your code. »

Different scenarios can be declared using the ‘case‘ keyword which provide a clear way of defining the enumeration perimeter.

enum Guitar {
case gibson
case martin
case epiphone
case fender
}

One of the most common types from the Swift standard library uses an enumeration.

— The Optional type

The Optional type from the standard library is actually an Enum composed with two related scenarios.

  • case none
  • case some(Wrapped)
public enum Optional<Wrapped> : ExpressibleByNilLiteral {// The compiler has special knowledge of Optional<Wrapped>, including the fact
/// that it is an `enum` with cases named `none` and `some`.
/// The absence of a value.
/// In code, the absence of a value is typically written using the `nil`literal rather than the explicit `.none` enumeration case.
case none/// The presence of a value, stored as `Wrapped`. case some(Wrapped)
}

A none case represents the absence of a value which, in Swift, stands for ‘nil‘.

Conversely, a some case represents an existing value which can also be retrieved — e.g ‘Wrapped‘ — , this is where associated values come into play.

— Associated values

The beauty of Swift enums is the ability to provide associated values — whether with a generic or a concrete type — according to each scenario.

The Optional enum takes a generic type ‘Wrapped‘ which represents the associated value of the ‘some‘ case.

When specializing, the actual ‘Wrapped‘ type will be defined.

public enum Optional<Wrapped> {
case none
case some(Wrapped)
}
var noValue: Optional<String> = .none// prints nil

Associated values also support labels which might improve readability when handling multiple cases with associated values.

public enum Optional<Wrapped> {
case none
case some(value: Wrapped)
}
var someValue: Optional<String> = .some(value: "Hello world !")

The ‘Optional<String>‘ type is the equivalent of ‘String?‘.

The ‘switch’ statement enables us to evaluate each scenario while retrieving any associated values.

switch someValue {
case .none:
print("Oops.. looks like there’s nothing in here.")
case let .some(value: element):
print(element)
}
// prints Hello world !

Enums in Swift provide more flexibility regarding the raw values as we can declare a specific default value for each case.

« The value can be a string, a character, or a value of any integer or floating-point type. »

— Raw Values

enum Guitar: String {
case gibson = “Gibson guitars”
case martin = “Martin guitars”
case epiphone = “Epiphone guitars”
case fender = “Fender guitars”
}

Default values are accessible using the rawValue property.

Guitar.gibson.rawValue// prints Gibson guitars

When using the rawValue property, Swift will automatically convert your case into a String thus we don’t need to explicitly declare the raw value.

enum Guitar: String {
case gibson
case martin
case epiphone
case fender
}
Guitar.gibson.rawValue// prints gibson

Using Int as default values comes in handy when implementing multiple sections within a tableView.

enum Guitar: Int {
case gibson
case martin
case epiphone
case fender
}
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let section = Guitar(rawValue: indexPath.row)! switch section {
case .gibson:
// gibson cell implementation
case .martin:
// martin cell implementation
case .epiphone:
// epiphone cell implementation
case .fender:
// fender cell implementation
}
}

— State

Enums are helpful when it comes to representing a view controller state.

A view controller that contains a table view might end up with different states.

  • Loading
  • Loaded
  • Empty
  • Error

These could easily be structured using an Enum with an associated value for the loaded case.

enum State<Content> {
case loading
case loaded([Content])
case empty
case error(Error)
}

Using such an enum in a view controller will improve code readability as it provides a set of predefined states which could be easily managed using a ‘switch case‘ statement.

— CaseIterable

enum Guitar: Int, CaseIterable {
case gibson
case martin
case epiphone
case fender
}

The CaseIterable protocol enables us to « access a collection of all of the type’s cases by using the type’s allCases property ».

let guitarsSections = Guitar.allCases// produces a [Guitar] type.

Using the CaseIterable protocol is quite convenient when populating our sections.

func numberOfSections(in tableView: UITableView) -> Int {
return Guitar.allCases.count
}

— Initialization

Enum, as first-class types, also provide an init() method.

enum Guitar: Int, CaseIterable {
case gibson
case martin
case epiphone
case fender
init() {
self = .gibson
}
}
let gibson = Guitar()// prints gibson

— Computed properties

Enums do not allow stored properties however you’re free to use computed properties or methods just like for a class or a struct.

enum Guitar: Int, CaseIterable {
case gibson
case martin
case epiphone
case fender
init() {
self = .gibson
}
var modelsCount: Int {
switch self {
case .gibson:
return 10
case .martin:
return 12
case .epiphone:
return 9
case .fender:
return 5
}
}
}
let gibson = Guitar()gibson.modelsCount// prints 10

— Coding

When it comes to encoding or decoding values, Enums which conform to the CodingKey protocol allow us to safely use the dictionary keys to match our data model.

"""
{
"user_identifier": 1,
"identifier": 3,
"title": "fugiat veniam minus",
"completed": false
}
"""struct Post {
let userId, id: Int
let title: String
let completed: String
enum CodingKeys: String, CodingKey {
case userId = "user_identifier"
case id = "identifier"
case title, completed
}
}

Not only can we use enums to safely retrieve the object keys but also the values.
Assuming our dictionary object stores a set of pre-defined values, we can create our Enum accordingly and decode it properly.

struct Post: Codable {
let userId: String
let id: String
let title: String
let status: Status
enum Status: String, Codable {
case pending = "pending"
case published = "published"
case saved = "saved"
}
}

By conforming our enumeration to the Codable protocol and giving each case a default value that matches the expected one, Swift will automatically create our Status enum.

Beware though that any keys/values mismatch will throw a Coding error.

— The Result type

Introduced with Swift 5, the Result type is an Enum composed with two cases representing a success and a failure.

The ‘Success’ and ‘Failure’ generic types are respectively associated to the ‘success‘ and ‘failure cases.

The Result type comes in handy when handling asynchronous network requests.

public enum Result<Success, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}

— Errors

Enums, as value types, do not support inheritance however they do support protocols.

Apple recommends using the Error protocol with enums to handle errors.

Swift’s enumerations are well suited to represent simple errors.

Create an enumeration that conforms to the Error protocol with a case for each possible error.

If there are additional details about the error that could be helpful for recovery, use associated values to include that information.

Let’s start off by implementing our enum with an Error protocol conformance.

enum Error: Swift.Error {
case badURL
case decoding(Swift.Error)
case emptyData
case network(Swift.Error)
}

Then, implementing our network request.

func fetch<T: Decodable>(
decodeType: [T].Type,
handler: @escaping (Result<[T], Error>
) -> Void) {
guard let url = URL(
string: “https://jsonplaceholder.typicode.com/posts"
) else {
handler(.failure(Error.badURL))
return
}
let request = URLRequest(url: url)
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
if let error = error {
handler(.failure(Error.network(error)))
}
guard let data = data else {
handler(.failure(Error.emptyData))
return
}
do {
let models = try JSONDecoder().decode(
decodeType,
from: data
)
handler(.success(models))
} catch let error {
handler(.failure(Error.decoding(error)))
}
}.resume()
}struct Post: Codable {
let title, body: String
let userId, id: Int
}
fetch(decodeType: [Post].self) { result in
switch result {
case let .failure(error):
print(error.localizedDescription)
case let .success(models):
print(models)
}
}

Thanks to the power of Enums, our errors will be properly handled using the combination of the Error and the Result types.

— Pattern matching

Whenever a ‘switch case‘ statement is assessed, Swift uses the ~= operator to find a matching value, this is called pattern matching.

If a match succeeds then the following function returns true.

func ~=(pattern: ???, value: ???) -> Bool

Let’s implement our own.

struct Greeting {
let word: Word
let country: Country
enum Word {
case bonjour
case hello
case hola
}
enum Country: String {
case FR
case EN
case ES
}
}
func ~=(pattern: String, value: Greeting) -> Bool {
return pattern == value.country.rawValue
}

Overloading the ~= operator will allow us to write our own ‘switch case‘ statement.

let greeting = Greeting(word: .bonjour, country: .FR)switch greeting {
case Greeting.Country.EN.rawValue:
print(greeting.word)
case Greeting.Country.FR.rawValue:
print(greeting.word)
case Greeting.Country.ES.rawValue:
print(greeting.word)
default:
fatalError(“Oops, wrong case.”)
}
// prints bonjour

Overloading the ~= operator enables us to provide pattern matching customization.

Conclusion

Enums are a key feature in the Swift language as they enable us to define a clear, well-defined context in which several related scenarios may occur.

While they allow us to write a more human-readable code, they also provide a powerful way to structure our app in a neat and concise way.

--

--

Jullian Mercier

Senior iOS engineer. jullianmercier.com. @jullian_mercier. Currently looking for new job opportunities.