Enums : Use Cases of Enums in everyday programming

K Samman
6 min readMar 14, 2023

--

Swift enums are a powerful feature that can help developers write more efficient and expressive code. However, they can also be complex and difficult to understand, especially for beginners , so why not we make it easy for developers to become master at enums increasing the complexity from beginner to advanced by diving into the world of Swift enums, showcasing their versatility and usefulness through real-life examples, exploring all types and subtypes, and sharing why enums should be your go-to choice in many programming scenarios.

Enums are a user-defined data type in Swift that allows us to group related values together. They are a useful and expressive way to represent different cases or states within our code. Enums can be as simple as defining a list of related constants or as advanced as containing associated values and methods.

How to declare an enum type?

Defining an enum is simple , just use enum keyword followed by the name of enum , then define some cases that represent different values for that enum , lets create a very simple enum.

enum CompassDirection {
case north
case east
case south
case west
}

Here we have defined CompassDirection enum , which represents different directions that can be shown on compass , and each case represents a different direction.

Now whenever in my code i have to define a direction of compass i can simple use CompassDirection.north or CompassDirection.east and so on.

//we define the direction on compass
let direction = CompassDirection.north

Enums with Associated Values

We can also use Associate values in enums which allows us to attach additional information to each case. For example, we can define an enum for different types of network requests, with associated values for the request URL, headers, and body. Here's an example:

enum NetworkRequest {
case get(url: URL)
case post(url: URL, headers: [String: String], body: Data)
}

We can now use the above enum to create a network request helper which takes in that enum and perform certain network tasks for us.

func sendRequest(_ request: NetworkRequest) {
var urlRequest = URLRequest(url: request.url)
switch request {
case .get:
urlRequest.httpMethod = "GET"
case let .post(_, headers, body):
urlRequest.httpMethod = "POST"
urlRequest.allHTTPHeaderFields = headers
urlRequest.httpBody = body
}

URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
// Handle the response or error
}.resume()
}

Using an enum with associated values like this can make our code more expressive and type-safe, by ensuring that we provide the necessary information for each type of request, and preventing errors from mismatched parameters.

Generics in Associated Values

Enums can be used to store associated values, which can be used to represent complex data types. For example, you can create an enum representing different types of geometric shapes (such as “circle”, “rectangle”, or “triangle”) and use associated values to store the specific dimensions of each shape.

enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case triangle(base: Double, height: Double)
}

func calculateArea(of shape: Shape) -> Double {
switch shape {
case let .circle(radius):
return .pi * radius * radius
case let .rectangle(width, height):
return width * height
case let .triangle(base, height):
return 0.5 * base * height
}
}

let circle = Shape.circle(radius: 2.0)
let rectangle = Shape.rectangle(width: 3.0, height: 4.0)
let triangle = Shape.triangle(base: 2.0, height: 3.0)

let circleArea = calculateArea(of: circle) // 12.566370614359172
let rectangleArea = calculateArea(of: rectangle) // 12.0
let triangleArea = calculateArea(of: triangle) // 3.0

Dependency Injection

We can use Enums for dependency Injection for example we can create an enum representing different types of networking clients and use it to inject the appropriate client into different parts of your app.

enum NetworkingClientType {
case alamofire
case urlSession
}

protocol NetworkingClient {
func get(url: URL, completion: @escaping (Result<Data, Error>) -> Void)
}

class AlamofireNetworkingClient: NetworkingClient {
func get(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
// make a network request using Alamofire
}
}

class URLSessionNetworkingClient: NetworkingClient {
func get(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
// make a network request using URLSession
}
}

class NetworkService {
let networkingClient: NetworkingClient

init(networkingClientType: NetworkingClientType) {
switch networkingClientType {
case .alamofire:
networkingClient = AlamofireNetworkingClient()
case .urlSession:
networkingClient = URLSessionNetworkingClient()
}
}

func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
networkingClient.get(url: url, completion: completion)
}
}

Conforming to CaseIterable Protocol

The CaseIterable protocol allows you to iterate over all the cases of an enum. Enums that conform to this protocol automatically gain a static allCases property, which returns an array of all the cases in the enum. This can be useful for writing generic code that works with any enum that conforms to CaseIterable.

Assuming we have a table where we have to display months of year , we can do this by using allCases property

enum Month: CaseIterable {
case january
case february
case march
case april
case may
case june
case july
case august
case september
case october
case november
case december
}

// Then in our View Controller we can use it as datasource
class MonthTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Month.allCases.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MonthCell", for: indexPath)
cell.textLabel?.text = Month.allCases[indexPath.row].capitalized
return cell
}
}

Enum Methods and Computed Properties

Swift enums can also have methods and computed properties, allowing us to add functionality directly to the enum. This is useful when we need to perform actions related to the enum or its cases:

enum TemperatureUnit {
case celsius
case fahrenheit
}

func convert(_ value: Double) -> Double {
switch self {
case .celsius:
return (value * 9 / 5) + 32
case .fahrenheit:
return (value - 32) * 5 / 9
}
}
}

Use Cases of Enums

Lets now discuss some of the daily use cases of enums.

Error handling

We can also use enums to represent error conditions in our applications. For example, we can create an enum representing different types of errors that can occur during network requests (such as “networkError”, “serverError”, or “authenticationError”) and use it to handle errors in a more structured way.

enum NetworkError: Error {
case unknownError
case invalidURL
case networkError(Error)
case serverError(Int)
case authenticationError
}

func fetchData(from url: URL, completion: @escaping (Result<Data, NetworkError>) -> Void) {
guard let request = URLRequest(url: url) else {
completion(.failure(.invalidURL))
return
}

URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(.networkError(error)))
return
}

guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.unknownError))
return
}

if (200..<300).contains(httpResponse.statusCode) {
if let data = data {
completion(.success(data))
return
} else {
completion(.failure(.unknownError))
return
}
} else if httpResponse.statusCode == 401 {
completion(.failure(.authenticationError))
return
} else {
completion(.failure(.serverError(httpResponse.statusCode)))
return
}
}.resume()
}

let url = URL(string: "https://example.com/data")!

fetchData(from: url) { result in
switch result {
case .success(let data):
// handle successful response
break
case .failure(let error):
// handle error
break
}
}

Handling API response status codes

One of the most common use case is we use enums for handling api response , for example.

enum HTTPStatusCode: Int {
case ok = 200
case created = 201
case badRequest = 400
case unauthorized = 401
case notFound = 404
case internalServerError = 500
}

func handleResponse(statusCode: HTTPStatusCode) {
switch statusCode {
case .ok, .created:
print("Request was successful")
case .badRequest:
print("Bad request")
case .unauthorized:
print("Unauthorized access")
case .notFound:
print("Resource not found")
case .internalServerError:
print("Internal server error")
}
}

let currentStatusCode: HTTPStatusCode = .ok
handleResponse(statusCode: currentStatusCode)

Representing user roles in an authentication system

Enums can be used to define distinct user roles, making it easier to manage access control and permissions in your application.

enum UserRole {
case guest
case member
case admin
}

func checkPermissions(for role: UserRole) {
switch role {
case .guest:
print("Guests have limited access to the system.")
case .member:
print("Members have access to most features.")
case .admin:
print("Admins have full access to the system.")
}
}

let currentUserRole: UserRole = .member
checkPermissions(for: currentUserRole)

above we have defined an enum called UserRole with three cases: guest, member, and admin. The checkPermissions function takes a user role as a parameter and prints out the corresponding permissions based on the role.

Enums offer several benefits that make them an attractive choice for developers:

  • Use them to improve code readability by providing a clear and concise way to represent related values.
  • Enhance safety by restricting values to a predefined set, reducing the likelihood of bugs.
  • It can have raw values, associated values, methods, and computed properties, making them versatile and expressive.
  • Enums are self-documenting, which helps in understanding the code and makes maintenance easier.

--

--

K Samman

Tech Lead at a Leading Consultancy in United Kingdom