Advanced and Practical Enum usage in Swift

Protocols

released Fri, 01 Mar 2019
Swift Version 5.0

Protocols

We already mentioned the similarity between the struct and enum types. In addition to the ability to add methods, Swift also allows you to use Protocols and Protocol Extensions with enums.

Swift protocols define an interface that types can conform to. In this case our enum can conform to it. For a start, let's take a protocol from the Swift standard library.

CustomStringConvertible is a type with a customized textual representation suitable for printing purposes:

protocol CustomStringConvertible {

   var description: String { get }

}

It has only one requirement, namely a getter for a string. We can implement this on an enum quite easily:

enum Trade: CustomStringConvertible {

    case buy, sell

    var description: String {

        switch self {

        case .buy: return \"We're buying something\"

        case .sell: return \"We're selling something\"

        }

    }

}

Some protocol implementations may need internal state handling to cope with the requirements. Imagine a protocol that manages a bank account:

protocol AccountCompatible {

   var remainingFunds: Int { get }

   mutating func addFunds(amount: Int) throws

   mutating func removeFunds(amount: Int) throws

}

You could easily fulfill this protocol with a struct, but in the context of your application, an enum is the more sensible approach.

However, you can't add properties like var remainingFunds: Int to an enum, so how would you model that? The answer is actually easy, you can use associated values for this:

enum Account {

   case empty

   case funds(remaining: Int)

   case credit(amount: Int)



   var remainingFunds: Int {

     switch self {

     case .empty: return 0

     case .funds(let remaining): return remaining

     case .credit(let amount): return amount

     }

   }

}

To keep things clean, we can then define the required protocol functions in a protocol extension on the enum:

extension Account: AccountCompatible {



   mutating func addFunds(amount: Int) {

     var newAmount = amount

     if case let .funds(remaining) = self {

       newAmount += remaining

     }

     if newAmount < 0 {

       self = .credit(newAmount)

     } else if newAmount == 0 {

       self = .empty

     } else {

       self = .funds(remaining: newAmount)

     }

   }



   mutating func removeFunds(amount: Int) throws {

     try self.addFunds(amount * -1)

   }



}



var account = Account.funds(remaining: 20)

try? account.addFunds(amount:10)

try? account.removeFunds(amount:15)

As you can see, we implemented all the protocol requirements by storing our values within our enum cases. A very nifty side effect of this is, that now you can test for an empty account with a simple pattern match all over your code base. You don't have to see whether the remainingFunds are zero.

We're also implementing the protocol in an extension. We'll learn more about extensions on enum types in the next chapter.