Swift - Enums

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‘ 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 UITableView.

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 JSONDecoding.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.