Articles, podcasts and news about Swift development, by John Sundell.

Avoiding having to recompute values within SwiftUI views

Published on 20 May 2021
Discover page available: SwiftUI

In general, using computed properties can be a great way to model view-specific data that we want to create on-demand, when need — especially within SwiftUI views, since each such view already uses a computed property (body) to implement its actual UI.

For example, here we’re using two computed properties to determine what title and button text that a UserSessionView should render based on the app’s current LoginState:

struct UserSessionView: View {
    var buttonAction: () -> Void
    @Environment(\.loginState) private var state

    var body: some View {
        VStack {
            Text(title)
                .font(.headline)
                .foregroundColor(.white)
            Button(buttonText, action: buttonAction)
                .padding()
                .background(Color.white)
                .cornerRadius(10)
        }
        .padding()
        .background(Color.blue)
        .cornerRadius(15)
    }

    private var title: String {
        switch state {
        case .loggedIn(let user):
            return "Welcome back, \(user.name)!"
        case .loggedOut:
            return "Not logged in"
        }
    }

    private var buttonText: String {
        switch state {
        case .loggedIn:
            return "Log out"
        case .loggedOut:
            return "Log in"
        }
    }
}

To learn more about the Environment property wrapper that’s being used above, check out my guide to SwiftUI’s state management system.

Implementing a SwiftUI view like that, using multiple computed properties, can often be a great approach — as it lets us keep our body implementations as simple as possible, and since it gives us a clear overview of how we’re computing the different content that a given view will render.

Recomputed properties

But, we also have to keep in mind that computed properties are just that — computed — there’s no form of caching or other kind of in-memory storage involved, meaning that each such values will always be recomputed each time that it’s being accessed.

That’s not an issue in our first example, because each of our properties can be quickly computed with constant (or O(1)) time complexity. However, let’s now take a look at another example, which is quite different in terms of performance characteristics, since we’re now computing a property by sorting a collection:

struct RemindersList: View {
    var items: [Reminder.ID: Reminder]

    var body: some View {
        List(sortedItems) { reminder in
            ...
        }
    }

    private var sortedItems: [Reminder] {
        items.values.sorted(by: {
    $0.dueDate < $1.dueDate
})
    }
}

On one hand, the above implementation could become quite problematic if the passed items dictionary ends up containing a very large amount of items, since that collection will be re-sorted every single time that we’ll access the sortedItems property. But, on the other hand, it’s a private property that’s only accessed from within our view’s body, and since our view doesn’t have any kind of mutable state, it’s not very likely that our property will actually be accessed that often.

However, that could quickly change if we were to add any kind of local state to our view — for example to enable the user to add a new Reminder using an inline TextField:

struct RemindersList: View {
    var items: [Reminder.ID: Reminder]
    var newItemHandler: (Reminder) -> Void

    @State private var newItemName = ""

    var body: some View {
        VStack {
            List(sortedItems) { reminder in
                ...
            }
            HStack {
    TextField("Add a new reminder", text: $newItemName)
    Button("Add") {
        newItemHandler(Reminder(name: newItemName))
    }
    .disabled(newItemName.isEmpty)
}
.padding()
        }
    }

    private var sortedItems: [Reminder] {
        items.values.sorted(by: {
            $0.dueDate < $1.dueDate
        })
    }
}

Now, every time that the user types a new character into the text field, our sortedItems property will be called and our items dictionary will be re-sorted. While that might not initially cause any obvious dips in performance, it’s a very inefficient implementation, and it’s very likely to cause problems at one point or another, especially for users with a large amount of reminders.

It’s important to remember that, although SwiftUI does use a type-based diffing algorithm to determine what underlying views to redraw for each state change, and does what it can to ensure that our UI remains performant, it’s not magic — if we write highly inefficient code, there’s not much that SwiftUI can do to fix that.

Going back to the source

So how can we fix this issue? One way would be to go back to the root source of our items data and update it to perform the necessary sorting right up front. Doing that has two main benefits — one, it lets us perform that operation once, instead of during every single view update, and two, it moves what is essentially a model-level operation into our actual model layer. Big win! Here’s what that could look like if we’re using Combine to load our items through some form of model controller:

func loadItems() -> AnyPublisher<[Item], Error> {
    controller
        .loadReminders()
        .map { items in
    items.values.sorted(by: {
        $0.dueDate < $1.dueDate
    })
}
        ...
        .eraseToAnyPublisher()
}

With the above change in place, we can now simply make our RemindersList view accept a pre-sorted array of reminders which we can just render as-is:

struct RemindersList: View {
    var items: [Reminder]
    ...

    var body: some View {
        VStack {
            List(items) { reminder in
                ...
            }
            ...
        }
    }
}

However, while the above approach is certainly preferable in many cases, there might be good reasons why we were modeling our core model collection as a dictionary, rather than using an array, and changing that might not be so simple, or even feasible at all. So let’s also explore a few other options as well.

Initialization logic

One way that we could solve our problem at the view level itself is by still having our RemindersList view accept a dictionary, just like before, but to instead perform our sorting within our view’s initializer, rather than using a computed property — for example like this:

struct RemindersList: View {
    var items: [Reminder]
    var newItemHandler: (Reminder) -> Void

    init(items: [Reminder.ID: Reminder],
     newItemHandler: @escaping (Reminder) -> Void) {
    self.items = items.values.sorted(by: {
        $0.dueDate < $1.dueDate
    })
    self.newItemHandler = newItemHandler
}

    @State private var newItemName = ""

    ...
}

With the above change in place, we’re now only sorting our collection once per RemindersList instance, rather than after every key stroke, without actually having to change the way our view is created or how our app’s data is managed. So while my general recommendation is to keep initializers focused on simple setup work, rather than performing data mutations, that’s a tradeoff that we might be willing to make in this case.

Worth keeping in mind, though, is that if the parent of our RemindersList view does update (or more specifically, if that parent’s body property gets re-evaluated), then a new instance of our view is likely going to be created, meaning that we’ll once again perform our item sorting operation.

Basically, when writing code within SwiftUI views, it’s close to impossible to gain complete control over when and how that code will be executed. After all, a core part of the design of a declarative UI framework like SwiftUI is that the framework takes care of orchestrating all of our UI updates for us.

So if we wanted to improve our control over the lifecycle of our actual model logic, then a better approach will likely be to move that logic out from our view implementations and into objects that we have complete control over.

Dedicated model logic

One way to do that would be to use something like a view model to encapsulate the logic associated with handling our items array. If we then make that view model an ObservableObject, then we’ll be able to easily observe it and connect to it within our SwiftUI views:

class RemindersListViewModel: ObservableObject {
    @Published private(set) var items: [Reminder]

    init(items: [Reminder.ID: Reminder]) {
        self.items = items.values.sorted(by: {
            $0.dueDate < $1.dueDate
        })
    }

    func addItem(named name: String) {
        ...
    }
}

With the above in place, we can now simplify our view quite heavily, and we’re also free to choose how we want to manage the above RemindersListViewModel ourselves, without having to take view updates and other SwiftUI implementation details into account:

struct RemindersList: View {
    @ObservedObject var viewModel: RemindersListViewModel
    @State private var newItemName = ""

    var body: some View {
        VStack {
            List(viewModel.items) { reminder in
                ...
            }
            HStack {
                TextField("Add a new reminder", text: $newItemName)
                Button("Add") {
                    viewModel.addItem(named: newItemName)
                }
                .disabled(newItemName.isEmpty)
            }
            .padding()
        }
    }
}

Very nice! That of course doesn’t mean that every single view within our app now needs to have a view model. It just so happens that in this particular case, a view model turned out to be a quite nice solution to our problem, since it enabled us to move our view-related model logic out from our view hierarchy itself (which also heavily improves that code’s testability).

Conclusion

Recomputing some of our view-related values every time that a SwiftUI view updates is typically not an issue. After all, that’s the way that each view’s body property works, and as long as those computations can happen quickly (and ideally, with constant time complexity) then we’re not very likely to run into any kind of major performance issues.

However, that’s not always the case, and sometimes we might need to be particularly careful with how we consume our model data within our views, especially if doing so involves any kind of potentially heavy operations that could slow down our overall UI performance.

Hopefully this article has illustrated my overall approach to solving those kinds of problems, and if you have any questions, comments or feedback, then feel free to reach out via either Twitter or email.

Thanks for reading!