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