Top iOS Interview Questions on Enumeration in Swift

Top iOS Interview Questions on Enumeration in Swift
iOS Interview Questions on Enumeration in Swift - Swift Anytime

Q1. What is an enumeration?

An enumeration is a common type that defines a group of related values. Enums are used to represent a set of distinct cases, and each case can have an associated value or a raw value.

Here's how to declare an enumeration:

enum NetworkError {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
}

// How to create a variable?
let error: NetworkError = .serverNotFound
print(error) // Output: serverNotFound

In this case, error is of type NetworkError, and it can only hold one of the four values specified in the NetworkError enumeration.

Q2. How do you declare a raw value in enumeration?

In enumeration , you can declare a raw value by assigning each case a pre-defined raw value of a compatible data type. Raw values are useful when you want to associate simple values, such as integers or strings, with each case.

Here's an example of a raw value named NetworkError with associated raw values:

enum NetworkError: Int {
    case noInternet = 1001
    case serverNotFound = 404
    case unauthorized = 401
    case internalServerError = 500
}

let error: NetworkError = .serverNotFound
print(error.rawValue) // Output: 404

In this example, we've declared the NetworkError enum with raw values of type Int. Each case is associated with an integer error code.

Q3. Explain the difference between a raw value and an associated value.

Enums can have associated values or raw values, and these two concepts serve different purposes.

Raw Value: A raw value is a predefined value that you assign to each case of an enumeration when you define it. Raw values are of the same type, and they must be unique within the enumeration.

Here's an example with raw values:

enum NetworkError: String {
    case noInternet = "No internet connection"
    case serverNotFound = "Server not found"
    case unauthorized = "Unauthorized access"
    case internalServerError = "Internal server error"
}

let error = NetworkError.serverNotFound
print(error.rawValue) // Output: "Server not found"

Raw values are typically used for situations where you have a finite set of possible values that are known in advance and don't change during runtime.

Associated Value: It allow you to associate additional data with each case of an enumeration. Associated values are used when the data associated with each case can vary, and you need to store different types of information for different cases.

Here's an example with associated values:

enum NetworkError {
    case noInternet
    case serverNotFound(url: String)
    case unauthorized(username: String)
    case internalServerError(statusCode: Int)
}

let serverError = NetworkError.serverNotFound(url: "base_url")
let unauthorizedError = NetworkError.unauthorized(username: "user_name")

The key difference between raw values and associated values is that raw values are fixed, predefined values associated with each case and are of the same type, while associated values allow you to associate variable data of different types with each case.

Q4. How do you create a custom initializer for an enumeration with associated values?

To create a custom initializer for an enumeration with associated values, you can define an initializer method inside the enum itself.

Here's an example of enum with a custom initializer:

enum NetworkError: Error {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
    case customError(message: String)
    
    init(errorCode: Int) {
        switch errorCode {
        case 401:
            self = .unauthorized
        case 404:
            self = .serverNotFound
        case 500:
            self = .internalServerError
        default:
            self = .customError(message: "Unknown error with code \(errorCode)")
        }
    }
}

let error = NetworkError(errorCode: 404) // Creates a .serverNotFound error
let customError = NetworkError(errorCode: 403) // Creates a .customError with a custom message

In the above example, we've defined a custom initializer init(errorCode:) that takes an integer errorCode as a parameter. This initializer allows you to create a NetworkError instance based on an error code.

Q5. What are the enum methods?. When and how would you use them?

In enum, you can create associated methods, also known as instance methods, for each case of an enumeration. These methods are specific to each case and allow you to encapsulate behavior associated with that particular case.

This is a powerful way to attach behavior to enum cases and make your code more organized and readable.

enum NetworkError {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
    
    func errorMessage() -> String {
        switch self {
        case .noInternet:
            return "No internet connection."
        case .serverNotFound:
            return "Server not found."
        case .unauthorized:
            return "Unauthorized access."
        case .internalServerError:
            return "Internal server error."
        }
    }
}

let error = NetworkError.noInternet
let errorMessage = error.errorMessage()
print(errorMessage) // Output: "No internet connection."

In this example, we've added an errorMessage() method to the NetworkError enum. Each case of the enum has its own implementation of this method, which returns an appropriate error message for that case.

Q6. How can you iterate through all cases of an enumeration?

To iterate through all cases of an enumeration in Swift, you can use the CaseIterable protocol, which automatically provides a allCases property that contains an array of all the enumeration cases.

enum NetworkError: CaseIterable {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
}

// Accessing all cases
let allNetworkErrors = NetworkError.allCases

// Iterating through all cases
for error in allNetworkErrors {
    switch error {
    case .noInternet:
        print("No Internet error")
    case .serverNotFound:
        print("Server not found error")
    case .unauthorized:
        print("Unauthorized error")
    case .internalServerError:
        print("Internal server error")
    }
}

/*
 Output:
 No Internet error
 Server not found error
 Unauthorized error
 Internal server error
 */

In this code, NetworkError conforms to the CaseIterable protocol, which means it will automatically generate the allCases property containing an array of all the cases defined in the enumeration.

The CaseIterable protocol allows an enumeration to automatically generate a collection of all its cases. This protocol was introduced in Swift 4.2 and later versions.

Q7. Why we should use enums where possible?

Enums offers several advantages in terms of code clarity, maintainability, and safety.

enum NetworkError {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
}

Code Clarity: Enums provide a way to create a clear and concise representation of a finite set of related values. In the NetworkError enum, it's immediately evident that the possible error scenarios related to network communication are noInternet, serverNotFound, unauthorized, and internalServerError.

Type Safety: Enums are strongly typed, which means that the compiler ensures that you handle all possible cases. This prevents runtime errors related to unexpected values. For instance, if you try to handle a NetworkError without covering all cases, the compiler will issue an error, prompting you to handle the new case appropriately.

Testing and Debugging: Enums simplify testing and debugging because they make it easier to simulate different error scenarios. You can create mock objects with specific enum cases to ensure your error-handling code behaves as expected under various conditions.

Q8. Explain the concept of recursive enumeration.

A recursive enumeration is an enumeration that can have associated values of its own type, allowing it to represent hierarchical or nested data structures. This can be particularly useful when you have a data structure that may contain instances of itself. It allows you to create more complex and flexible data models.

enum NetworkError {
    case noInternet
    case serverNotFound
    case unauthorized
    case internalServerError
    case customError(String)
    indirect case chainedError(NetworkError, NetworkError)
}

let firstError = NetworkError.customError("Authentication failed")
let secondError = NetworkError.noInternet
let chainedError = NetworkError.chainedError(firstError, secondError)

With this recursive enumeration, you can handle more complex error scenarios in your networking code. For instance, you could create a tree-like structure of errors where each chainedError case contains multiple sub-errors.

Recursive enumerations can be especially handy when you need to represent hierarchical or nested data structures, and they make your code more expressive and flexible.

The indirect keyword is used to indicate that an enumeration or an associated value of an enumeration should be treated as having an "indirect" relationship. This is particularly important when you're dealing with recursive data structures, such as a recursive enumeration.

In Swift, enums are value types, which means that they are typically allocated on the stack, and their size must be known at compile time. However, when you have a recursive data structure, the size of an enum can depend on how many times it refers to itself. This creates a problem because it would lead to infinite recursion in determining the size of the enum, causing a compile-time error.

By using the indirect keyword, you're essentially telling the Swift compiler to store the associated value of the enumeration case on the heap rather than the stack. This dynamic allocation on the heap allows for recursive structures because the size of the enum is no longer fixed at compile time.

Here's an example using a simple recursive list data structure:

indirect enum LinkedList<T> {
    case empty
    case node(T, LinkedList<T>)
}

In this example, we have a LinkedList enum that can either be empty or a node containing a value of type T and another LinkedList (which may also be a node or empty). By marking the enum as indirect, we can create linked lists of arbitrary length without worrying about stack overflow errors.

We hope that these interview questions will help you in your iOS interview preparation and also strengthen your basics on Enumeration in Swift. We have launched our new e-book "Cracking the iOS Interview" with Top 100 iOS Interview Questions & Answers. Our book has helped more than 372 iOS developers in successfully cracking their iOS Interviews.

Grab your copy now and rock your next iOS Interview!