The power of Enum in Swift

Swift Value types Enum Jan 13, 2021 · 8 min read

Value types in Swift are simple but yet really powerful. An enum is not an exception. You can solve quite a lot of programming problems just by using enums. The purpose of enums is to group a set of a related but fixed amount of values in one place under one common type and do different types of operations by using them.

Syntax

Let's look at the real-life problem you can come across while developing mobile apps. Suppose, you have an iOS app that supports different types of authentication methods via Email, Github, LinkedIn, and Twitter. The way you represent them in Swift is pretty simple and straightforward.

enum SignInMethod {
    case email
    case github
    case linkedIn
    case twitter
}

Of course, you can simplify it even more by using the shortcut method that requires separating them with commas under one case.

enum SignInMethod {
    case email, github, linkedIn, twitter
}

But, it might lead to readability problems when you have a lot of cases with custom raw values. I will explain what the raw values are a little bit later. That's why I will opt out using the shortcut method most of the time, if not all while coding.

Raw value

The example we saw previously is simple. But, there is one drawback. The cases don't have corresponding underlying values. What I mean is when you want to send an API request, say, to your backend API, you always have to check the case first and associate an appropriate value for that case manually.

func signIn(with method: SignInMethod) {
    let methodValue: Int

    switch method {
    case .email:
        methodValue = 0
    case .github:
        methodValue = 1
    case .linkedIn:
        methodValue = 2
    case .twitter:
        methodValue = 3
    }

    // Send an API request with the methodValue to your backend server
}

signIn(with: .github)

But, there is a solution for that. Enums can have raw values by inheriting from other value types such as Int, String, Character, etc. Here is an example with Int.

enum SignInMethod: Int {
    case email // 0
    case github // 1
    case linkedIn // 2
    case twitter // 3
}

By default, all cases have raw values of Int type automatically assigned starting from 0. If you want it to start from another value, say 1, just set it explicitly to the first case.

enum SignInMethod: Int {
    case email = 1 // 1
    case github // 2
    case linkedIn // 3
    case twitter // 4
}

If you want to use other types of values, for example, if your backend developers want you to send the values of String type instead of Int you can inherit from String so that all cases automatically have the corresponding values that are equal to the case names themselves.

enum SignInMethod: String {
    case email // email
    case github // github
    case linkedIn // linkedIn
    case twitter // twitter
}

What if your backend developers, especially Python/Django ones, want you to send snake_case values. It doesn't work for LinkedIn in our case. But, you can override the underlying raw value and make it snake_case.

enum SignInMethod: String {
    case email // email
    case github // github
    case linkedIn = "linked_in" // linked_in
    case twitter // twitter
}

When you want to process the authentication method sent by the backend in a form of String value, you can convert it to the enum SignInMethod type because working directly with strings is not safe and might result in runtime errors while enums are type-safe and checked at a compile-time.

enum SignInMethod: String {
    case email
    case github
    case linkedIn = "linked_in"
    case twitter
}

let method = SignInMethod(rawValue: "email")!
print(method.rawValue) // email

Methods

Now, you need to configure production and staging endpoints for each authentication method. You can do it by creating an instance method. Enums support instance and type methods like structs and classes.

enum SignInMethod: String {
    case email
    case github
    case linkedIn = "linked_in"
    case twitter

    func endpoint(isProduction: Bool) -> String {
        switch self {
        case .email:
            return isProduction ? "https://mydomain.com/oauth" : "https://staging.mydomain.com/oauth"
        case .github:
            return isProduction ? "https://github.com/oauth" : "https://staging.github.com/oauth"
        case .linkedIn:
            return isProduction ? "https://linkedin.com/oauth" : "https://staging.linkedin.com/oauth"
        case .twitter:
            return isProduction ? "https://twitter.com/oauth" : "https://staging.twitter.com/oauth"
        }
    }
}

let method: SignInMethod = .twitter
print(method.endpoint(isProduction: true)) // https://twitter.com/oauth
print(method.endpoint(isProduction: false)) // https://staging.twitter.com/oauth

CaseIterable

When you want to show a list of authentication methods to your users that your app supports, you can show them manually.

print(SignInMethod.email.rawValue)
print(SignInMethod.github.rawValue)
print(SignInMethod.linkedIn.rawValue)
print(SignInMethod.twitter.rawValue)

But, the problem with this way is that when you add a new authentication method, say Google, you also have to manually add print(SignInMethod.google.rawValue) to the list where you show them to your users. Luckily, there is a more elegant way of doing this by conforming to CaseIterable protocol and iterate over all cases.

enum SignInMethod: String, CaseIterable {
    case email
    case github
    case linkedIn
    case twitter
}

for method in SignInMethod.allCases {
    print(method.rawValue)
}

All is good until your designers scold you for showing them lowercased instead of capitalized. To solve this problem, we can create a new computed property named title where we check all of the cases with a switch statement and return the corresponding values capitalized. When you iterate over cases, use the title property instead of rawValue.

enum SignInMethod: String, CaseIterable {
    case email
    case github
    case linkedIn
    case twitter

    var title: String {
        switch self {
        case .email:
            return "Email"
        case .github:
            return "Github"
        case .linkedIn:
            return "LinkedIn"
        case .twitter:
            return "Twitter"
        }
    }
}

for method in SignInMethod.allCases {
    print(method.title)
}

Associated values and RawRepresentable

Let's do some advanced stuff. It is common that third-party authentication methods provide you with an appID that you must send when making a sign-in request to their authentication servers in order to generate an access token that is needed to make API calls to their Public API. The way to achieve this is to use associated values for each case. But, we might not have an appID for the email authentication method because it is your own way of authentication with your backend server. I have come across many APIs that don't have an appID for their client apps. That's why I am providing this kind of example. But, I think it is wrong. It is always better to have an appID for each of your client apps due to some important reasons like security. For example, you might want to block your client apps by appID that were exploited or reverse-engineered by hackers. Anyway, here is how to associate an appID with third-party authentication methods except for the email one which belongs to your app and server.

enum SignInMethod: String, CaseIterable { // Error: 'SignInMethod' declares raw type 'String', but does not conform to RawRepresentable and conformance could not be synthesized. Type 'SignInMethod' does not conform to protocol 'CaseIterable'.
    case email
    case github(appID: String) // Error: Enum with raw type cannot have cases with arguments
    case linkedIn(appID: String) // Error: Enum with raw type cannot have cases with arguments
    case twitter(appID: String) // Error: Enum with raw type cannot have cases with arguments

    var title: String {
        switch self {
        case .email:
            return "Email"
        case .github:
            return "Github"
        case .linkedIn:
            return "LinkedIn"
        case .twitter:
            return "Twitter"
        }
    }

    var appID: String? {
        switch self {
        case .email:
            return nil
        case .github(let appID):
            return appID
        case .linkedIn(let appID):
            return appID
        case .twitter(let appID):
            return appID
        }
    }
}

let emailMethod: SignInMethod = .email
print(emailMethod.rawValue) // Error: Value of type 'SignInMethod' has no member 'rawValue'
print(emailMethod.title) // Email
print(emailMethod.appID) // nil

let linkedInMethod: SignInMethod = .linkedIn(appID : "ABCDE12345")
print(linkedInMethod.rawValue) // Error: Value of type 'SignInMethod' has no member 'rawValue'
print(linkedInMethod.title) // LinkedIn
print(linkedInMethod.appID) // Optional("ABCDE12345")

Unfortunately, once you start using associated values, you can't inherit from types like String, Int, and other value types that provide raw values automatically. Also, your enum can no longer conform to CaseIterable protocol to be able to iterate over cases. But, you can achieve the same result by manually creating a computed property named rawValue (you can name whatever you want) for each case and static var with an Array type to be able to iterate over cases. Since we only need to show the titles, we can simply create static var allTitles variable.

enum SignInMethod {
    case email
    case github(appID: String)
    case linkedIn(appID: String)
    case twitter(appID: String)

    static var allTitles: [String] { ["Email", "Github", "LinkedIn", "Twitter"] }

    var rawValue: String {
        switch self {
        case .email:
            return "email"
        case .github:
            return "github"
        case .linkedIn:
            return "linked_in"
        case .twitter:
            return "twitter"
        }
    }

    var title: String {
        switch self {
        case .email:
            return "Email"
        case .github:
            return "Github"
        case .linkedIn:
            return "LinkedIn"
        case .twitter:
            return "Twitter"
        }
    }

    var appID: String? {
        switch self {
        case .email:
            return nil
        case .github(let appID):
            return appID
        case .linkedIn(let appID):
            return appID
        case .twitter(let appID):
            return appID
        }
    }
}

let emailMethod: SignInMethod = .email
print(emailMethod.rawValue) // email
print(emailMethod.title) // Email
print(emailMethod.appID) // nil

let linkedInMethod: SignInMethod = .linkedIn(appID: "ABCDE12345")
print(linkedInMethod.rawValue) // linked_in
print(linkedInMethod.title) // LinkedIn
print(linkedInMethod.appID) // Optional("ABCDE12345")

for title in SignInMethod.allTitles {
    print(title)
}

Indirect

You can create a recursive enumeration when one or more cases have associated values whose type is the same as your enum type. I added a new authentication method with Stripe which requires another authentication method such as Github. To achieve this, you must mark the enum as indirect. Here is an example of how to do this.

indirect enum SignInMethod {
    case email
    case github(appID: String)
    case linkedIn(appID: String)
    case stripe(method: SignInMethod)
    case twitter(appID: String)

    static var allTitles: [String] { ["Email", "Github", "LinkedIn", "Stripe", "Twitter"] }

    var rawValue: String {
        switch self {
        case .email:
            return "email"
        case .github:
            return "github"
        case .linkedIn:
            return "linked_in"
        case .stripe:
            return "stripe"
        case .twitter:
            return "twitter"
        }
    }

    var title: String {
        switch self {
        case .email:
            return "Email"
        case .github:
            return "Github"
        case .linkedIn:
            return "LinkedIn"
        case .stripe:
            return "Stripe"
        case .twitter:
            return "Twitter"
        }
    }

    var appID: String? {
        switch self {
        case .email:
            return nil
        case .github(let appID):
            return appID
        case .linkedIn(let appID):
            return appID
        case .stripe(let method):
            return method.appID
        case .twitter(let appID):
            return appID
        }
    }
}

let method: SignInMethod = .stripe(method: .github(appID: "ABCDE12345"))
print(method.appID) // Optional("ABCDE12345")

Conclusion

You can see how powerful enums in Swift. When you come across programming problems that require a set of related but fixed amount of values under one common type, you now know that enums can be an ideal solution.