Enums in Swift
Enums (short for enumerations) in Swift allow you to define a common type for a group of related values and work with those values in a type-safe way within your code.
Enums in Swift can have associated values, raw values, and can also conform to protocols.
I will explain the different aspects of enums with detailed code examples below.
Basic Enum
A basic enum defines a group of related values. Here’s an example of a simple enum that represents the directions of a compass:
enum CompassDirection { case north case south case east case west } // Usage let direction = CompassDirection.north
Switch Statement with Enums
Enums work well with Swift’s switch statement, allowing you to match each enum case and perform different actions for each case:
switch direction { case .north: print("Heading North") case .south: print("Heading South") case .east: print("Heading East") case .west: print("Heading West") }
Associated Values
Associated values in Swift enums allow you to store additional information along with each case of the enum.
This is particularly useful when the cases of an enum need to carry different types or amounts of data.
By using associated values, enums can be more expressive and versatile.
Basic Example
Consider an enum that represents different types of barcodes. Each barcode type can have different associated values:
enum Barcode { case upc(Int, Int, Int, Int) case qrCode(String) } // Usage var productBarcode = Barcode.upc(8, 85909, 51226, 3) productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
In this example, the Barcode
enum has two cases: upc
and qrCode
. The upc
case has four associated integer values, while the qrCode
case has one associated string value.
Accessing Associated Values
You can access the associated values of an enum case using a switch statement or optional binding:
Using Switch Statement
switch productBarcode { case .upc(let numberSystem, let manufacturer, let product, let check): print("UPC: \(numberSystem), \(manufacturer), \(product), \(check)") case .qrCode(let code): print("QR Code: \(code)") }
Using Optional Binding
if case let .qrCode(code) = productBarcode { print("QR Code: \(code)") }
Associated Values with Different Data Types
Enums with associated values can store different types of data for each case. This can be useful for modeling complex data structures. Here’s an example of an enum that represents a response from a server:
enum ServerResponse { case success(String, Int) case error(String) } // Usage let successResponse = ServerResponse.success("Data received", 200) let errorResponse = ServerResponse.error("Not found") switch successResponse { case .success(let message, let statusCode): print("Success: \(message), Status code: \(statusCode)") case .error(let errorMessage): print("Error: \(errorMessage)") }
Using Associated Values in Methods
You can use associated values within methods to encapsulate functionality related to each case:
enum MathOperation { case addition(Int, Int) case subtraction(Int, Int) case multiplication(Int, Int) case division(Int, Int) func perform() -> Int { switch self { case .addition(let a, let b): return a + b case .subtraction(let a, let b): return a - b case .multiplication(let a, let b): return a * b case .division(let a, let b): return a / b } } } // Usage let operation = MathOperation.addition(5, 3) print("Result: \(operation.perform())") // Output: Result: 8
Nested Enums with Associated Values
Enums can be nested and can have associated values, providing even more flexibility:
enum Device { case phone(model: String) case laptop(model: String, isTouchscreen: Bool) enum OperatingSystem { case iOS case macOS case windows case linux } var os: OperatingSystem { switch self { case .phone: return .iOS case .laptop(let model, _): return model == "MacBook" ? .macOS : .windows } } } // Usage let myDevice = Device.laptop(model: "MacBook", isTouchscreen: false) print("Operating System: \(myDevice.os)") // Output: Operating System: macOS
Summary
Associated values in Swift enums provide a powerful way to represent a variety of related data in a type-safe manner. They allow enums to store additional information for each case, making them versatile and expressive. By using associated values, you can create more complex data structures and encapsulate related functionality within your enums, leading to cleaner and more maintainable code.
Raw Values
Raw values in Swift enums allow you to assign a default value to each case. These raw values can be of type String
, Character
, or any integer or floating-point number. Unlike associated values, where each case can have different types of additional data, raw values provide a common value type for all cases in the enum.
Here’s a detailed explanation of raw values with examples:
Basic Example
Here’s an example of an enum with raw values of type String
:
enum Planet: String { case mercury = "Mercury" case venus = "Venus" case earth = "Earth" case mars = "Mars" } // Usage let planet = Planet.earth print("Planet: \(planet.rawValue)") // Output: Planet: Earth
In this example, each case of the Planet
enum is assigned a raw value of type String
. The rawValue
property can be used to access the raw value associated with each case.
Implicit Raw Values
If the raw value type is an integer or string, and you do not explicitly assign raw values, Swift will automatically assign them. For strings, each case’s name is used as its raw value. For integers, the first case is assigned the value 0
by default, and subsequent cases are assigned values incremented by 1
.
String Example
enum Direction: String { case north case south case east case west } // Usage let direction = Direction.east print("Direction: \(direction.rawValue)") // Output: Direction: east
Integer Example
enum Day: Int { case sunday case monday case tuesday case wednesday case thursday case friday case saturday } // Usage let day = Day.friday print("Day: \(day.rawValue)") // Output: Day: 5
Initializing from Raw Values
You can initialize an enum instance using a raw value. This initializer returns an optional enum case because the raw value might not correspond to any case in the enum.
if let possiblePlanet = Planet(rawValue: "Earth") { print("Planet: \(possiblePlanet)") // Output: Planet: earth } else { print("No such planet") } if let day = Day(rawValue: 3) { print("Day: \(day)") // Output: Day: wednesday } else { print("No such day") }
Raw Values with Different Types
Raw values can be of any type that conforms to the RawRepresentable
protocol, such as Int
, Double
, Character
, etc.
Character Example
enum Alphabet: Character { case a = "A" case b = "B" case c = "C" case d = "D" } // Usage let letter = Alphabet.c print("Letter: \(letter.rawValue)") // Output: Letter: C
Double Example
enum Coin: Double { case penny = 0.01 case nickel = 0.05 case dime = 0.10 case quarter = 0.25 } // Usage let coin = Coin.quarter print("Coin value: \(coin.rawValue)") // Output: Coin value: 0.25
Practical Usage
Raw values are useful when you need a predefined value associated with each enum case, such as representing days of the week, HTTP status codes, or mapping to string identifiers.
HTTP Status Code Example :
enum HTTPStatusCode: Int { case ok = 200 case notFound = 404 case internalServerError = 500 } // Usage let statusCode = HTTPStatusCode.notFound print("Status code: \(statusCode.rawValue)") // Output: Status code: 404
Summary
Raw values in Swift enums provide a convenient way to associate constant values with each case. They can be of various types, including strings, integers, characters, and floating-point numbers. Raw values are useful for creating enums that map to external data sources or predefined sets of values, and they support automatic value assignment for string and integer types. By using raw values, you can simplify your code and make it more readable and maintainable.
Methods in Enums
Enums can have methods associated with them to encapsulate functionality related to the enum:
enum Beverage: String { case coffee, tea, juice func description() -> String { switch self { case .coffee: return "Coffee is a brewed drink." case .tea: return "Tea is an aromatic beverage." case .juice: return "Juice is a drink made from fruit." } } } // Usage let drink = Beverage.coffee print(drink.description())
Enum with Protocol Conformance
Enums can conform to protocols just like other types. This is useful for adding common functionality:
protocol Describable { func describe() -> String } enum Animal: Describable { case cat, dog, bird func describe() -> String { switch self { case .cat: return "A small domesticated carnivorous mammal." case .dog: return "A domesticated carnivorous mammal that typically has a long snout." case .bird: return "A warm-blooded egg-laying vertebrate animal." } } } // Usage let pet = Animal.dog print(pet.describe())
CaseIterable
protocol
The CaseIterable
protocol in Swift allows you to access a collection of all the cases of an enum.
This can be particularly useful when you need to iterate over all possible values of an enum.
To use CaseIterable
, simply declare your enum as conforming to the CaseIterable
protocol.
Here’s a detailed explanation and examples:
Using CaseIterable
Basic Example
First, let’s see a simple example of an enum conforming to CaseIterable
:
enum CompassDirection: CaseIterable { case north case south case east case west } // Usage for direction in CompassDirection.allCases { print(direction) }
In this example, CompassDirection
conforms to CaseIterable
, which automatically provides a static allCases
property containing a collection of all the cases in the enum. The for
loop then iterates over each direction and prints it.
Enum with Raw Values
CaseIterable
can also be used with enums that have raw values:
enum Beverage: String, CaseIterable { case coffee case tea case juice } // Usage for drink in Beverage.allCases { print(drink.rawValue) }
In this example, Beverage
is an enum with raw values of type String
. The CaseIterable
conformance allows you to iterate over all the cases and print their raw values.
Customizing allCases
By default, the allCases
property is automatically synthesized by the compiler. However, you can customize it if you need a different behavior. Here’s how:
enum Planet: CaseIterable { case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune static var allCases: [Planet] { return [.mercury, .venus, .earth, .mars, .jupiter, .saturn, .uranus, .neptune] } }
In this example, although the default synthesis of allCases
would suffice, you have the option to provide your own implementation if needed.
Practical Usage
Let’s consider a practical scenario where you might use CaseIterable
. Suppose you have an enum representing different types of notifications in an app, and you want to display all the notification types in a list:
enum NotificationType: CaseIterable { case email case sms case push } // Usage for notification in NotificationType.allCases { print("Notification type: \(notification)") } // Output: // Notification type: email // Notification type: sms // Notification type: push
This approach ensures that any changes to the enum, such as adding or removing cases, are automatically reflected wherever allCases
is used, making your code more maintainable.
Summary
The CaseIterable
protocol is a convenient way to work with all the cases of an enum in Swift. It provides an allCases
property that you can use to iterate over each case, making it easy to handle scenarios where you need to work with the full set of possible values of an enum. Whether for iterating over cases, generating test data, or populating UI components, CaseIterable
enhances the flexibility and usability of enums in Swift.
Read More