Advanced Enum (Enumerations) by Example - Swift Programming Language

1. Properties

We can't add actual stored properties to an enum, but we can create computed properties. The value of computed properties can be based on the enum value or enum associated value.

Let's create an enum called Device. It's contain a computed property called year, which return the first appearance of that device.

enum Device {  
    case iPad
    case iPhone

    var year: Int {
        switch self {
            case .iPhone: 
                return 2007
            case .iPad: 
                return 2010
        }
    }
}

let device = Device.iPhone  
print(device.year)  
//prints "2007"

2. Methods

We can also define methods in an enum. Methods in enums exist for every enum case. So if we want to have specific code for specific cases, we need a switch to determine the correct code path for that specific case.

enum Device {  
    case iPad
    case iPhone

    func introduced() -> String {
        switch self {
            case .iPhone: 
                return "\(self) was introduced 2007"
            case .iPad: 
                return "\(self) was introduced 2010"
        }
    }
}

let device = Device.iPhone  
print (device.introduced())  
//prints "iPhone was introduced 2007"

3. Nested Enums

We can nest enumerations one inside another, this allows you to structure hierarchical enums to be more organized and clear.

3.1 Create a Nested Enumerations

Imagine a character in a Roll Playing Game. Each character can have a weapon, all characters have access to the same set of weapons. So, Let's create an enum called Character. It contains other enums called, Weapon and Helmet.

enum Character {  
    enum Weapon {
        case bow
        case sword
        case dagger
    }

    enum Helmet {
        case wooden
        case iron
        case diamond
    }

    case thief(weapon: Weapon, helmet: Helmet)
    case warrior(weapon: Weapon, helmet: Helmet)

    func getDescription() -> String {
        switch self {
            case let .thief(weapon, helmet):
                return "Thief chosen \(weapon) and \(helmet) helmet"
            case let .warrior(weapon, helmet):
                return "Warrior chosen \(weapon) and \(helmet) helmet"
        }
    }
}

3.2 Initialization

Now, We have a hierarchical system to describe the various items that our character has access to.

let helmet = Character.Helmet.iron  
print(helmet)  
//prints "iron"

let weapon = Character.Weapon.dagger  
print(weapon)  
//prints "weapon"

let character1 = Character.warrior(weapon: .sword, helmet: .diamond)  
print(character1.getDescription())  
// prints "Warrior chosen sword and diamond helmet"

let character2 = Character.thief(weapon: .bow, helmet: .iron)  
print(character2.getDescription())  
//prints "Thief chosen bow and iron helmet"

4. Containing Enums

We can also embed enums in structs or classes. This pattern is very important for app business logic. Also, helps in keeping related information together.

4.1 Create a Struct which containing Enums

with our previous example:

struct Character {  
    enum CharacterType {
        case thief
        case warrior
    }
    enum Weapon {
        case bow
        case sword
        case dagger
    }
    let type: CharacterType
    let weapon: Weapon
}

4.2 Initialization

To initialize a Character, we have to choose CharacterType and Weapon.

let character = Character(type: .warrior, weapon: .sword)  
print("\(character.type) chosen \(character.weapon)")  
//warrior chosen sword

5. Mutating Method

We may create a mutating function that can set the implicit self parameter to be a different case from the same enumeration.

The example below defines an enumeration for a three-state switch. The switch cycles between three different power states (off, low and high) every time its next() method is called.

enum TriStateSwitch {  
    case off
    case low
    case high
    mutating func next() {
        switch self {
            case .off:
                self = .low
            case .low:
                self = .high
            case .high:
            self = .off
        }
    }
}

var ovenLight = TriStateSwitch.off  
ovenLight.next() // ovenLight is now equal to .low  
ovenLight.next() // ovenLight is now equal to .high  
ovenLight.next() // ovenLight is now equal to .off again  

6. Static Method

We can also create static methods in enum. Let's create an enum called, Device. It contains a static function which returns Device based on the name of the parameter used in the function.

enum Device {  
    case iPhone
    case iPad

    static func getDevice(name: String) -> Device? {
        switch name {
            case "iPhone":
                return .iPhone
            case "iPad":
                return .iPad
            default:
                return nil
        }
    }
}

if let device = Device.getDevice(name: "iPhone") {  
    print(device) 
    //prints iPhone
}

7. Custom Init

We may add a custom init method to create an object of our choice.

enum IntCategory {  
    case small
    case medium
    case big
    case weird

    init(number: Int) {
        switch number {
            case 0..<1000 :
                self = .small
            case 1000..<100000:
                self = .medium
            case 100000..<1000000:
                self = .big
            default:
                self = .weird
        }
    }
}

let intCategory = IntCategory(number: 34645)  
print(intCategory)  
//prints medium

8. Protocol Oriented Enum

Swift protocols define an interface or type that other structures can conform to. In this case our enum will conform to it.

In order to understand protocol with enums, We'll create a Game. In which, a player could be either .dead or .alive. When alive, the player has a number of hearts/lives. If the number goes to zero, the player becomes dead.

8.1 Create Protocol

Let's create a protocol called, LifeSpan. It contains one property, numberOfHearts and two mutating functions which will be used to increase/decrease a player's heart.

protocol LifeSpan {  
    var numberOfHearts: Int { get }
    mutating func getAttacked() // heart -1
    mutating func increaseHeart() // heart +1
}

8.2 Create Enum

Now, create an enum called Player. It conforms to protocol LifeSpan. There are two cases. The object can be either dead or alive. The alive case contains an associated value called currentHeart. It also contains a gettable computed property, numberOfHearts. numberOfHearts is determined based on whether self is case or alive.

enum Player: LifeSpan {  
    case dead
    case alive(currentHeart: Int)

    var numberOfHearts: Int {
        switch self {
            case .dead: return 0
            case let .alive(numberOfHearts): return numberOfHearts
        }
    }

    mutating func increaseHeart() {
        switch self {
            case .dead:
                self = .alive(currentHeart: 1)
            case let .alive(numberOfHearts):
                self = .alive(currentHeart: numberOfHearts + 1)
        }
    }

    mutating func getAttacked() {
        switch self {
            case .alive(let numberOfHearts):
                if numberOfHearts == 1 {
                    self = .dead
                } else {
                    self = .alive(currentHeart: numberOfHearts - 1)
                }
            case .dead:
            break
        }
    }
}

8.3 Play Game

Let's play the Game.

var player = Player.dead // .dead

player.increaseHeart()  // .alive(currentHeart: 1)  
print(player.numberOfHearts) //prints 1

player.increaseHeart()  // .alive(currentHeart: 2)  
print(player.numberOfHearts) //prints 2

player.getAttacked()  // .alive(currentHeart: 1)  
print(player.numberOfHearts) //prints 1

player.getAttacked() // .dead  
print(player.numberOfHearts) // prints 0  

9. Extensions

We can also extend the enums. The most apparent use case for this is keeping enum cases and methods separate, so that a reader of your code can easily digest the enum and after that move on to the methods:

enum Entities {  
    case soldier(x: Int, y: Int)
    case tank(x: Int, y: Int)
    case player(x: Int, y: Int)
}

Now, we can extend this enum with methods:

extension Entities {  
    mutating func attack() {}
    mutating func move(distance: Float) {}
}

We can also write extensions to add support for a specific protocol:

extension Entities: CustomStringConvertible {  
    var description: String {
        switch self {
            case let .soldier(x, y): return "Soldier position is (\(x), \(y))"
            case let .tank(x, y): return "Tank position is (\(x), \(y))"
            case let .player(x, y): return "Player position is (\(x), \(y))"
        }
    }
}

10. Generic Enums

Enums can also be defined over generic parameters. Like structs, classes, and functions, the syntax looks identical.

enum Information<T1, T2> {  
    case name(T1)
    case website(T1)
    case age(T2)
}

Let us initialize.

let info = Information.name("Bob") // Error  

The compiler is able to recognize T1 as String based on "Bob". However, the type of T2 is not defined yet. Therefore, you must define both T1and T2 explicitly as shown below.

let info =Information<String, Int>.age(20)  
print(info) //prints age(20)  

11. Recursive Enums

Let's create an enum called, ArithmeticExpression. It contains three cases with associated types. Two of the cases contain its own enum type, ArithmeticExpression. The indirect keyword tells the compiler to handle this enum case indirectly.

Enums and cases can be marked indirect, which causes the associated value for the enum to be stored indirectly, allowing for recursive data structures to be defined.

indirect enum ArithmeticExpressions {  
    case number(Int)
    case addition(ArithmeticExpressions, ArithmeticExpressions)
    case multiplication(ArithmeticExpressions, ArithmeticExpressions)
}

func evaluate(_ expression: ArithmeticExpressions) -> Int {  
        switch expression {
            case let .number(value):
                return value
            case let .addition(left, right):
                return evaluate(left) + evaluate(right)
            case let .multiplication(left, right):
                return evaluate(left) * evaluate(right)
    }
}


let expression = evaluate(ArithmeticExpressions.addition(.number(1), .number(2)))  
print(expression) //prints 3  

You can download the swift playground of all above examples from Here




Related Article