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