Weekly Swift articles, podcasts and tips by John Sundell.

Avoiding deeply nested Swift code

Published on 17 May 2020
Basics article available: SwiftUI

Code style and structure are arguably two of the trickiest topics within programming in general. Not because they require any particular skills or vast experience building software, but because they’re so incredibly subjective in nature. What one person might consider the most readable and elegantly structured code in the world, another person might find cryptic and complicated.

However, there are a few techniques that can be employed to make the code that we write more generally accessible to other people (even if they might disagree with our particular choice of style). This week, let’s take a look at a few such techniques, that all have the same goal — reducing the amount of indentation within our code.

Early returns and code extraction

Let’s start by taking a look at a relatively simple example of how using early returns within functions can have quite a big impact on the overall readability of our code — even without any additional changes to the way our expressions are formed, or the way our APIs are designed.

As an example, let’s say that we’ve extended a DocumentLibraryViewController with a short but useful method that filters an array of documents to only include ones that are unread and accessible by the current user:

extension DocumentLibraryViewController {
    func unreadDocuments(from list: [Document]) -> [Document] {
        list.filter { document in
            if user.accessLevel >= document.requiredAccessLevel {
                if !user.readDocumentIDs.contains(document.id) {
                    if document.expirationDate > Date() {
                        return true
                    }
                }
            }
    
            return false
        }
    }
}

While the above code works as intended, the fact that it’s quite heavily indented arguably makes it more difficult to “mentally parse” than it has to be. So let’s see if we can fix that by using Swift’s guard statement to instead exit out of our method as early as possible — like this:

extension DocumentLibraryViewController {
    func unreadDocuments(from list: [Document]) -> [Document] {
        list.filter { document in
            guard user.accessLevel >= document.requiredAccessLevel else {
                return false
            }
    
            guard !user.readDocumentIDs.contains(document.id) else {
                return false
            }
    
            return document.expirationDate > date
        }
    }
}

Although our logic remains exactly the same, it’s now a bit easier to quickly get an overview of what our function’s actual conditions are — as they’re now listed from top to bottom at the same level of indentation.

Apart from making our code easier to read and understand for others, one major benefit of doing the above kind of restructuring is that it often lets us discover new ways in which we could improve the overall structure of our code even further.

For example, since two of our function’s three conditions operate on a Document instance, we could move those conditions into a separate extension instead — which not only makes it easier for that code to be read and tested in isolation, it also enables us to reuse it within other parts of our code base as well:

extension Document {
    func isAccessible(for user: User, date: Date = .init()) -> Bool {
        guard user.accessLevel >= requiredAccessLevel else {
            return false
        }

        return expirationDate > date
    }
}

Note how we now also inject the current Date, rather than creating it inline, which further improves the testability of our code (for example through “time traveling”).

Along the same lines, since our main unreadDocuments(from:) method is directly operating on an array of documents — and only requires a User instance and the current Date apart from that — we could also choose to extract it out from our DocumentLibraryViewController, and instead implement it within an extension on any Sequence that contains Document elements, like this:

extension Sequence where Element == Document {
    func unread(for user: User, date: Date = .init()) -> [Document] {
        filter { document in
            guard document.isAccessible(for: user, date: date) else {
                return false
            }

            return !user.readDocumentIDs.contains(document.id)
        }
    }
}

Unit testing the above API is now simply a matter of creating a collection of Document values, along with a User instance and a date according to what part of our logic that we wish to test, and then verifying that our method returns an array of correctly filtered documents.

With the above tweaks in place, we’ve now not only modeled our logic as separate functions that can be independently used and tested, we’ve also made our call sites read really nicely as well:

let unreadDocuments = allDocuments.unread(for: user)

While we ended up doing much more than just reducing the amount of indentation above, it all started when we decided to do something about a heavily indented piece of code. That’s the magic of refactoring — in that revising an implementation in order to simplify it often reveals brand new venues for improvement as well.

Untangling nested logic branches

However, not all logic can be modeled as a simple sequence of boolean conditions — sometimes we need to handle a much larger amount of states and permutations, which in turn might require us to branch our logic into several nested if and else statements.

For example, here we’re working on a ProductViewController that uses a private update method to populate its views once it received a new Product model. Since the overall state that our UI will end up in depends on a number of factors — such as whether a user is currently logged in, if there are any discounts available, and so on — we’ve currently ended up with a quite lengthly, nested implementation that looks like this:

class ProductViewController: UIViewController {
    private let sessionController: SessionController
    private lazy var descriptionLabel = UILabel()
    private lazy var favoriteButton = UIButton()
    private lazy var priceView = PriceView()
    private lazy var buyButton = UIButton()
    
    ...

    private func update(with product: Product) {
        if let user = sessionController.loggedInUser {
            if let discount = product.discount(in: user.region) {
                let lowerPrice = product.price - discount
                priceView.amountLabel.text = String(lowerPrice)
                priceView.discountLabel.text = String(discount)
            } else {
                priceView.amountLabel.text = String(product.price)
                priceView.discountLabel.text = ""
            }

            if user.favoriteProductIDs.contains(product.id) {
                favoriteButton.setTitle("Remove from favorites",
                    for: .normal
                )
            } else {
                favoriteButton.setTitle("Add to favorites",
                    for: .normal
                )
            }

            favoriteButton.isHidden = false
        } else {
            favoriteButton.isHidden = true
            priceView.amountLabel.text = String(product.price)
            priceView.discountLabel.text = ""
        }

        priceView.currencyLabel.text = product.currency.symbol
        descriptionLabel.text = product.description
    }
}

At first glance, it might seem like the above is the best that we can do given the number of conditions and separate states that we need to handle — but, just like earlier, once we start breaking our implementation apart into separate pieces, we’ll likely discover new approaches that we could take.

To get started, let’s move all of our product-bound logic into a private extension on Product instead. That way, we can perform those computations in isolation, and simply return values that represent the current state that our app is in — like this:

// By keeping this extension private, we're able to implement
// logic that's specific to our product view within it:
private extension Product {
    typealias PriceInfo = (price: Double, discount: Double)

    func priceInfo(in region: Region?) -> PriceInfo {
        guard let discount = region.flatMap(discount) else {
            return (price, 0)
        }

        return (price - discount, discount)
    }

    func favoriteButtonTitle(for user: User) -> String {
        if user.favoriteProductIDs.contains(id) {
            return "Remove from favorites"
        } else {
            return "Add to favorites"
        }
    }
}

The above ProductInfo type is implemented as a tuple, which provide a great way to create lightweight types in Swift.

Next, let’s encapsulate all of our view state computation within a dedicated type. We’ll call it ProductViewState, given that its only purpose will be to represent the current state of our product view in a read-only fashion. We’ll initialize it with a Product and an optional User, and will then compute our view’s current state using the private Product APIs that we just implemented:

struct ProductViewState {
    // By making all of our properties constants, the compiler
    // will generate an error if we forget to assign a value to
    // one of them (including those that are optionals):
    let priceText: String
    let discountText: String
    let currencyText: String
    let favoriteButtonTitle: String?
    let description: String

    init(product: Product, user: User?) {
        let priceInfo = product.priceInfo(in: user?.region)
        priceText = String(priceInfo.price)
        discountText = priceInfo.discount > 0 ? String(priceInfo.discount) : ""
        currencyText = product.currency.symbol
        favoriteButtonTitle = user.map(product.favoriteButtonTitle) ?? ""
        description = product.description
    }
}

We could’ve also modeled the above type as a read-only view model, by calling it ProductViewModel instead.

With the above two pieces in place, we can now go back to our view controller’s update method and heavily simplify it. Gone are all the nested if and else statements, and we no longer have any duplicate assignments to the same property. Plus, our implementation can now easily be read from top to bottom, since we’ve extracted all of the decision-making conditions into separate, smaller functions:

class ProductViewController: UIViewController {
    ...

    private func update(with product: Product) {
        let state = ProductViewState(
            product: product,
            user: sessionController.loggedInUser
        )

        priceView.amountLabel.text = state.priceText
        priceView.discountLabel.text = state.discountText
        priceView.currencyLabel.text = state.currencyText

        favoriteButton.setTitle(state.favoriteButtonTitle, for: .normal)
        favoriteButton.isHidden = (state.favoriteButtonTitle == nil)

        descriptionLabel.text = state.description
    }
}

Just like when we previously extracted our Document-related methods into separate APIs, a big benefit of the above kind of refactoring is that it makes unit testing so much simpler — as all of our logic is now structured as pure functions that can be individually developed and tested.

SwiftUI views

Finally, let’s take a look at how we can employ some of the same techniques that we used above when constructing views using SwiftUI.

Since SwiftUI’s DSL uses closures to encapsulate the construction of our various views, it’s quite easy to end up with an implementation that’s heavily indented, even when building a relatively simple list view — such as this one:

struct EventListView: View {
    @ObservedObject var manager: EventManager

    var body: some View {
        NavigationView {
            List(manager.upcomingEvents) { event in
                NavigationLink(
                    destination: EventView(event: event),
                    label: {
                        HStack {
                            Image(event.iconName)
                            VStack(alignment: .leading) {
                                Text(event.title)
                                Text(event.location.name)
                            }
                        }
                    }
                )
            }
            .navigationBarTitle("Upcoming events")
        }
    }
}

Although it might seem like the above kind of “code pyramid” is inevitable when working with nested SwiftUI views, there are once again a number of ways that we could heavily flatten (and simplify) our code in this case.

Just like how we previously extracted parts of our various logic into separate types and functions, we can do the same thing here as well — for example by creating a dedicated type for rendering the rows that appear within the above list. Since SwiftUI views are just lightweight descriptions of our UI, that can most often be done by simply moving the code in question into a new View-conforming type — like this:

struct EventListRow: View {
    var event: Event

    var body: some View {
        HStack {
            Image(event.iconName)
            VStack(alignment: .leading) {
                Text(event.title)
                Text(event.location.name)
            }
        }
    }
}

Apart from creating stand-alone view types, using private factory methods can also be a great way to split a SwiftUI view up into separate pieces. For example, here’s how we could define a method that wraps an instance of our new EventListRow type within a NavigationLink, making it ready to be displayed within our list:

private extension EventListView {
    func makeRow(for event: Event) -> some View {
        NavigationLink(
            destination: EventView(event: event),
            label: {
                EventListRow(event: event)
            }
        )
    }
}

The cool thing is that, with just the above two tweaks in place, we can now remove almost all of the indentation from our EventListView — by passing the above makeRow method as a first class function when creating our List, like this:

struct EventListView: View {
    @ObservedObject var manager: EventManager

    var body: some View {
        NavigationView {
            List(manager.upcomingEvents, rowContent: makeRow)
                .navigationBarTitle("Upcoming events")
        }
    }
}

While always removing all sources of indentation is hardly a good goal to have — as the right amount of indentation can also help us improve the readability of a given type or function — it’s interesting to see just how much flexibility Swift gives us in terms of code structure, which in turn lets us adapt our choice of structure within each given situation.

Conclusion

Although it might seem like a heavy amount of indentation is inevitable within certain parts of a code base, that’s actually rarely the case — as there are often multiple approaches that we can take to structure our logic in ways that not only reduces the amount of indentation needed, but also makes the overall flow of our logic easier to follow.

Once we start untangling a piece of heavily indented code, it’s also likely that we’ll discover new ways to structure, reuse and test that code as well — which can make refactoring a very natural and neat way to come up with shared abstractions, and to improve the overall testability of a code base.

But again, not all indentation deserves to be removed, but hopefully this article has given you some insights into how I approach these kinds of refactoring and maintenance tasks. Feel free to let me know what you think — along with your questions, comments and feedback — either via Twitter or email.

Thanks for reading! 🚀