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 T1 and 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



Discussion

Read Community Guidelines
You've successfully subscribed to Developer Insider
Great! Next, complete checkout for full access to Developer Insider
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.