Hallo allemaal, Leo hier. Today we will discuss a topic that is very common for iOS developers which is manipulating data which enums and common mistakes to avoid.

When I’m hiring someone, I prioritize understanding the fundamentals over specialized knowledge. Questions about intricate coding problems or the exact steps in an app lifecycle are less important to me. As long as candidates grasp the basics well, they’re fit for 99% of roles.

During my own interview prep, I encountered an article that really resonated with me: companies aren’t just looking for the brightest or the most technically proficient individuals; they’re looking for people they actually want to work with. Unfortunately, this interpersonal compatibility isn’t something you can train for—it’s the RNG element of interviews.

I once applied for a mid-level position and was rejected, only to be accepted months later for a senior role at the same company. This taught me how little control we have over the hiring process and you should never let that affect you in a negative way. You won’t fully know what interviewers are looking for, so the best approach is simply to be authentic, study a lot, and hope for the best.

Some organizations, like Google, strive to eliminate bias by using straightforward technical questions, like those on Leetcode. This method has its advantages. However, even at Google, a poor attitude can disqualify a candidate.

Now, observing the difference between junior and senior professionals, I see that the major distinction isn’t technical ability. It’s their attitude towards challenges that sets them apart. We will write more about this later.

Last week, we wrote about how to animate number changes in SwiftUI using the contentTransition modifier. By adding this modifier, developers can create smooth transitions for dynamic content in their apps, enhancing visual engagement and user experience.

Thinking about the network layer, in the previous weeks, we wrote about how to monitor network connectivity in SwiftUI using NWPathMonitor and the @Observable annotation. It provides a concise guide to implementing a network monitor that checks internet availability and cellular data usage, enhancing user experience by proactively managing connectivity issues.

No more talking, let’s code! But first…

 

Painting of The Day

The painting today is called The Chess Players, a 1929 art piece by John Lavery.

Sir John Lavery, an esteemed Irish painter, gained fame for his portraits and wartime scenes. His career took off after painting Queen Victoria in 1888, leading to high-profile connections and a knighthood. Active during the Irish War of Independence, he later contributed to Irish galleries. Lavery’s second marriage to Hazel Martyn was notable, influencing many of his works. He died in 1941. The picture shows the Hon. Margaret and the Hon. Rosemary Scott-Ellis, daughters of the 8th Baron Howard de Walden.

I chose this picture because when you are playing chess your mistakes are really clear and today we will talk about not so easy to spot mistakes.

 

The Problem – Common Error Using Enum

You want to use enum in a realiably and easy to maintain way in your codebase.

 

The common mistakes that we will discuss today are:

  1. Lack of Cohesion when Mapping Values
  2. Lazy Switch
  3. Assuming Automatic Raw Value Incrementation


Lets dive into each one of them.

 

Lack of Cohesion when Mapping Values To Enum

So let’s imagine that you have an app that has the following struct for a user login that will be received from the backend: 

struct PersonNetworkData: Codable {
    let id: Int
    let name: String
    let status: PersonNetworkStatus
}

enum PersonNetworkStatus: Codable {
    case success
    case processing
    case postProcessing
    case failure
    case legacy
}

This information is the raw information that we receive from the server, which means it is ready to use by a view. 

The network layer should be able to have changes without affecting other parts of the software, that’s why we separate the concerns. Our app’s responsibility is to get the PersonData object and transform it into something that will be useful for the view. 

Now let’s imagine that the view only cares about the name and 3 users’ statuses, success, failure, and processing. All others could be simply implied as success. Since we agreed that the view layer should not know about what doesn’t concern it, how should we map this object? 

At first glance, you might something like this:

struct PersonInfo {
    let name: String
    let status: PersonLoginStatus
    init(personNetworkData: PersonNetworkData) {
        self.name = personNetworkData.name
        self.status = PersonLoginStatus(personNetworkStatus: personNetworkData.status)
    }
}

enum PersonLoginStatus {
    case success
    case failure
    case processing
    case unavailable
    
    init(personNetworkStatus: PersonNetworkStatus) {
        switch personNetworkStatus {
        case .success:
            self = .success
        case .processing:
            self = .processing
        case .failure:
            self = .failure
        default:
            self = .unavailable
        }
    }
}

Since we just care about 3 status on the view side we don’t want to parse anything else from the network layer. This solves the problem that we stated above however it creates a new problem. Can you spot the mistake in the mapping above?

We will talk about cohesion now.

A crucial aspect of OOP is ensuring that objects exhibit high cohesion, meaning each object should be designed with a well-defined purpose, encapsulating all and only those properties and methods that are directly related to its intended functionality.

High cohesion within objects is crucial as it enhances the maintainability, modularity, and clarity of the code. When objects are highly cohesive, they are easier to understand, debug, and test because their behavior is specific and focused. 

But what is the problem with the code above? 

The problem is that we are mixing concepts here. When we map three of the network statuses directly from to statuses we are telling the future devs that’s the way we want to map and the view needs to know every status that comes from the backend. However, the last case that we introduced breaks that train of thought.

And this problem only gets bigger, imagine if we are using this abstraction all over the app, and suddenly we need the postProcessing case from the network layer. We would have to change every place to include this case for the enum. This behavior is really good but in this case, we could have saved some trouble by searching in the codebase and checking whether it is used or not.

There are various solutions for this case. I’ll present two but keep in mind that there are others.

The first one is to map 1-1 the network statuses to view statuses. The drawback is that every time the network layer changes, you also need to change your whole mapping system.

The first solution is something like this: 
 

enum PersonLoginStatus {
    case success
    case processing
    case postProcessing
    case failure
    case legacy
    
    init(personNetworkStatus: PersonNetworkStatus) {
        switch personNetworkStatus {
        case .success:
            self = .success
        case .processing:
            self = .processing
        case .postProcessing:
            self = .postProcessing
        case .failure:
            self = .failure
        case .legacy:
            self = .legacy
        }
    }
}

 Although this solves the cohesion problem, in my opinion, this creates a high coupling problem. Now both layers are tightly together. This is not necessarily an improvement because we create another problem but this could be something that you want in the end if you want to have full control of what is coming from the backend.

The second solution solves the cohesion problem gracefully and that will be grouping as logical clusters. The idea of grouping to the unavailable is not bad, the only problem is that we were mixing concepts with the other ones up there, so the idea is go full on the available and unavailable cases. 

Check the code below for the second mapping solution:

 

enum PersonLoginStatus {
    case available(PersonLoginStatusDetails) // now we have two defined states that has internal cohesion between them
    case unavailable
    
    init(personNetworkStatus: PersonNetworkStatus) {
        switch personNetworkStatus {
        case .success:
            self = .available(.success)
        case .processing:
            self = .available(.processing)
        case .failure:
            self = .available(.failure)
        default:
            self = .unavailable
        }
    }
    
    enum PersonLoginStatusDetails {
        case success
        case failure
        case processing
    }
}

With this, we have the right cohesion and low coupling that we wanted from the beginning. Since now we just have the available and unavailable cases, everything comes down to which one your view can or cannot handle. The con about this approach is that we needed one more structure to specialize what kind of available that was. In my opinion is a really small drawback to now have everything well explained with the types that we created.

 

Lazy Switch or Using Default: Clause When You Shouldn’t

I think every iOS developer already had this idea: I’ll just handle the cases that I need and the rest of the cases of the enum go to the default clause. 

This is a really nice feature that Swift provides to us and we should use it wisely. One of the most powerful tools in the iOS world is that we can enforce the type in a Switch Case. This means that all of the enum cases should be handled by the developers. The problem is when someone think that you should use this every time you can, and you shouldn’t. 

The code below is a perfect example of it: 

enum PersonNetworkStatus: Codable {
    case success
    case processing
    case postProcessing
    case failure
    case legacy
}

let status = PersonNetworkStatus.legacy

switch status {
    case .success:
        print("Yey success")
    case .legacy:
        print("Oh lets go to the legacy flow")
    default: // this is the lazy switch case
        print("not handled at all")
}

Observe in the example below that we are just using success and legacy cases. All the other statuses we are just adding to the default. Imagine that now I add a new status like: facebookFlow, where in the app should I handle it?

With the lazy enum, we don’t have the compiler helping us and we need to dig into the code ourselves and have some risky side effects of changing things that we didn’t want for start.

The question here is: When should we use the default case?

My recommendation is that you should use the default case in things that you don’t have control over and/or change a lot. For example, using enums from other modules, teams, or anything that you don’t have control and can break your codebase without previous notice. In those cases since you really don’t know what to expect you just use it as a backup solution. Or if the enum keeps growing or changing all the time, it’s less hustle to fix when you need the specific things from it.

When it includes business rules and I have control over the statuses, I always like to explicitly add all the cases for the enums. I love the peace of mind that I have when I add a new case and the compiler says when I have to change. 

So remember, the default enum case is not a silver bullet and can be a shoot in your foot.

 

Assuming Automatic Raw Value Incrementation

This is a classic one. It’s amazing that enum has an automatic raw value incrementation with Integers. But you shouldn’t rely on that value, EVER.

Check the code below:

 

enum PersonNetworkStatusCode: Int {
    case success
    case processing
    case postProcessing
    case failure
    case legacy
}

let statusCode = PersonNetworkStatusCode.processing.rawValue
print(statusCode) // prints 1

The code above is adding automatically a zero-based Integer raw value for each one of the cases. So success will be zero, processing will be one, postProcessing will be two and so on. This feature is amazing and handy when you need to know the order of the cases in the enum. For example, if you have the order of actions based on the order that they are declared in the enum, you can do that automatically using that feature.

The problem starts when you start making conclusions that could not be always true. Check the code below:

enum PersonNetworkStatusCode: Int {
    case success
    case processing
    case postProcessing
    case failure
    case legacy
}

let statusCode = PersonNetworkStatusCode.processing.rawValue

if statusCode == 1 { // OMG
    print("We are in the processing state")
}

print(statusCode)

You can spot the problem in the code above, right? Assuming a automatic assigned raw value to take any action in your app is the receipt for the disgrace. 

Never, ever, put yourself in this situation.

This has no fix other than just using the case itself: 

let statusCode = PersonNetworkStatusCode.processing.rawValue

if PersonNetworkStatusCode(rawValue: statusCode) == .processing {
    print("We are in the processing state")
}

print(statusCode)

Don’t leave it up to luck if your code will run correctly or not, use the type system in your favor.

Aaaand we are done for today!

 

Conclusion – How to Avoid Common Enum Mistakes in Swift?

In wrapping up our discussion on common enum mistakes in Swift, it’s clear that the strength of enums isn’t just in their syntax, but in how they are implemented to streamline and safeguard your coding practices. Avoiding pitfalls like lazy switching, uncontrolled raw value assumptions, and poor cohesion in mapping can significantly elevate the reliability and maintainability of your app.

As you continue developing your iOS applications, remember that enums, when used judiciously, serve not only as a tool for organizing code but as a means of enforcing a more rigorous type safety and logical consistency throughout your projects.

Keep these guidelines in mind, code thoughtfully, and always strive for clarity and efficiency in your implementations.

Fellow iOS Developers, that’s all. I hope you liked reading this article as much as I enjoyed writing it.

If you want to support this blog you can Buy Me a Coffee or say hello on Twitter. I’m available on LinkedIn or send me an e-mail through the contact page.

You can likewise sponsor this blog and help our community to grow.

Thanks for the reading and…

That’s all folks.

Image credit: Featured Painting