RSSBlog

Swift: Using Enums to Write Safer Code

If you have been programming for a while, especially for iOS, you've probably built plenty of View Controllers. The joke goes that 'MVC' actually stands for "Massive View Controllers". The reason is that it is very easy to just put a lot of code inside view controllers, making them absolutely humongous.

I will not teach you how to fix that now, because that's especially tricky, but I'm going to give you a short hint that might help you make that whole mess a lot more reliable.

Suppose you have a view controller called ObjectViewController that displays data regarding an object of type MyObject. The view controller is presented from a list (say, a table view controller), and the only thing it knows about upon loading is the id of the object you want to show (say, a DB index).

Many things can happen along the way of properly displaying the object data. For example:

  • Internet access might have dropped, so you can't fetch the data for that object;
  • Internet access might be slow, and you need to provide information to the user that the data is being loaded;
  • The ID you received could be incorrect, or invalid;
  • The response from the server might be garbled.

In order to build a great UI, you should never do any of the following:

  • Block the UI thread;
  • Display a blank view;
  • Display an error message that is too generic;
  • Display no error message;
  • Block the UI thread (in case you missed it).

These requirements, coupled with the scenarios I described above, give you a difficult task of building a UI that makes sense. Because of that, it is especially important that you design your view controllers to absolutely minimize the amount of assumptions you have to make about the data you have (or don't have), in order to reduce the amount of possible failure points.

The lazy & easy way

You might be very tempted to do the following:

  • Have a objectId property that stores the id of the object you want to display;
  • Have a object property that stores the object once it loads.
  • Have a error property that stores an error if it happens.

The object property has to be optional, because you need to fetch it. The objectId should arguably be optional as well because it is not known until the view controller is presented (the user made a selection in the list, and the view controller was queued). The error has to be optional because it might happen or not.

The class would look more or less like this:

class ObjectViewController: UIViewController
{
    var objectIdToShow: Int? = nil
    var objectToShow: MyObject? = nil
    var error: NSError? = nil

    // Called when the user makes a selection
    func setObjectIdToShow(_ objectId: Int)
    {
        self.objectIdToShow = objectId
    }

    override func viewWillAppear(_ animated: Bool)
    {
        super.viewWillAppear(animated)

        if let objectId = objectIdToShow, RemoteStorage.loadObjectWithId(objectId, completion:
        {
            (error: Error?, loadedObject: MyObject?) in

            if error == nil, let myObject = loadedObject
            {
                // ... store the object ...
                self.objectToShow = myObject
                updateViews()
            }
            else
            {
                // ... show "loading failed" error message ...
                self.error = error
                updateViews()
            }
        }
        else
        {
            // ... show "bad id" error message ...
            updateViews()
        }

        // ... show "loading..." animation...
        updateViews()
    }

    private func updateViews()
    {
        // ... make your views show data about the object, or an error
        // using a bucket-load of if-else statements ...
    }

    // ... rest of your VC ...
}

This is a horrible way of doing it for a multitude of reasons:

  • Just with the optional properties, you have several permutations of failure cases;
  • You have to manually keep track of what happened, and when;
  • You have to manually update the views;
  • If the code path has to change at any point in the future, all these "manual" tasks need to be inspected to make sure you didn't miss any possibility.

In general, to write reliable code, you strive to reduce the amount of possible scenarios. One way of doing that is by synchronizing all known possible cases into a single point of knowledge (let's call it the truth point), and make anything that needs to change (depending on the object being loaded, errors, and etc) rely on that only.

Just like traffic lights synchronize different roads in a crossing, you need something that synchronizes the current "truth", and make sure there's no other way to infer the "truth" from anywhere else (for example. checking if a variable is nil or not).

Enums, they know the truth

The reason why I say that is because enums are a closed set. There are only a certain known amount of possibilities, and if you adapt your code to be ready for all of them, it will never misbehave. What you want to do is create a "State" enumeration that defines all the possible scenarios of your View Controller, and attach every dynamic condition of your code to it.

For our example, let's define the following set of possible scenarios:

  1. The view controller was not initialized;
  2. A object id was assigned to the view controller, but fetching of the object data has not yet initialized;
  3. The fetching of the object data has begun, and we are waiting on a response;
  4. The fetching of the object has finished, we have parsed the data successfully, and the views can update to display their data;
  5. The fetching of the object has failed, and we know (at least approximately) why.

These are all the possibilities we will design our new view controller around. Anything and everything that happens will need to fall into one of these cases. We will employ some Swift features to help us make this as elegant as possible.

First, let's create an enum called ObjectViewControllerState with one case for each of the situations described above:

enum ObjectViewControllerState
{
    case uninitialized
    case objectIdSet(Int)
    case fetchingObjectData
    case objectReady(MyObject)
    case error(Error)
}

What the heck are those.. parameters? In the cases??

Those are called "Associated Values", and are one of the crazy-powerful features of Swift. From Apple's documentation:

[An Associated Value] enables you to store additional custom information along with the case value, and permits this information to vary each time you use that case in your code.

In other words, the enum not only tells you the exact current state of the view controller, but it also attaches a value to it. The magic of it all is that, for example, the Error object attached to the case error enum entry is only accessible, or even, only visible, if the state of the view controller is such that an error has occurred.

Therefore, it is impossible to reach a state where an error exists, but an object MyObject is also present (which would be a terrible inconsistency to handle).

Read Apple's documentation about Enums linked above if you are unfamiliar with how they work in Swift.

Now that we have our enum cases, we define a non-optional property in the view controller, and set it to a known initial value:

class ObjectViewController: UIViewController
{
    private var state: ObjectViewControllerState = .uninitialized

...we add the method to setup the view controller...

    // Called when the user makes a selection
    func setObjectIdToShow(_ objectId: Int)
    {
        self.state = .objectIdSet(objectId)
    }

...start fetching the object (if we can!), and update the state once we know what happened...

    override func viewWillAppear(_ animated: Bool)
    {
        super.viewWillAppear(animated)

        if case .objectIdSet(let objectId) = self.state
        {
            // We have the object id! We can fetch the object now. We update the state to the "loading" state.
            self.state = .fetchingObjectData

            // loadObjectWithId should dispatch on a background thread and call the completion back on the main thread,
            // and thus should not block the main thread
            RemoteStorage.loadObjectWithId(objectId, completion:
            {
                (error: Error?, loadedObject: MyObject?) in

                if let error = error
                {
                    // We check for an error first, and completely ignore the object parameter if an error is set:
                    self.state = .error(error)
                }
                else if let myObject = loadedObject
                {
                    // Everything seems to be in place, we update the state to the "ready" state
                    self.state = .objectReady(myObject)
                }
                else
                {
                    // This is an internal error. Let's switch to the error state
                    self.state = .error(NSError(domain: "com.example.MyApp",
                                                code: 124,
                                                userInfo: [NSLocalizedDescriptionKey: "Internal error: could not fetch object information."]))
                }
            }
        }
        else
        {
            // We have no id.. we should switch to the error state
            self.state = .error(NSError(domain: "com.example.MyApp",
                                        code: 123,
                                        userInfo: [NSLocalizedDescriptionKey: "Internal error: no object to display."]))
        }
    }

And here is the magical part that brings it all together: The self.state property is what synchronizes the whole view controller now, and we have to make sure that what the user sees is what the code actually represents. Instead of adding a "bucket-load of if-else statements" and calls to updateViews() everywhere, we add a simple listener to the self.state property that is automatically called whenever the value of self.state changes. That's the best way of making sure the views are synchronized with the state:

class ObjectViewController: UIViewController
{
    private var state: ObjectViewControllerState = .uninitialized
    {
        didSet
        {
            updateViews()
        }
    }

Now, we use a simple switch-case statement to update the views accordingly:

    private func updateViews()
    {
        switch self.state
        {
        case .objectReady(let myObject):
            // ... update views with data from the object ...

        case .error(let error):
            // ... show error message ...

        case .uninitialized, .objectIdSet, .fetchingObjectData:
            // ... from the user's perspective, all these states look the same: show a loading view ...
        }
    }

Conclusion

That's it. It really is! And our code is a lot more reliable now, because:

  1. We have tied the view controller behavior to a specific set of possibilities using the ObjectViewControllerState enum;
  2. We synchronize the views using self.state and its didSet listener, making sure the views always display what the code is doing;
  3. We only store information when it makes sense to store it (error, object, object id);
  4. We avoid if-else chains that are difficult to read and can always miss a specific permutation of possibilities by using a concise switch-case statement instead.

This is just a very brief explanation of how you can use Enums in swift to make your code more reliable. Keep in mind that you can use this architecture anywhere else, like for example in the completion blocks of the hypothetical RemoteStorage class from the examples, instead of an error and a MyObject pair of objects.

When you start using these architectures in your code, the difference might seem small at first, but the reliability will be increased by a lot, and your code will feel more tame and comprehensive. I hope you can find use for these ideas in your code!

Keep coding and have fun!

Part 2

I have now written a part two to this post. Read it here.