Using the .task view modifier

Previously we've discussed the issue of SwiftUI eagerly creating views when using NavigationLink with the destination parameter, and how to work around that using NavigationLink with the value parameter as introduced in macOS 13/iOS 16. For applications deploying on earlier versions of those operating systems , the .onAppear view modifier is a great a work around. It allows you to perform more costly computing tasks only when the view has been displayed.

Let's look at how we can use the .task view modifier to further break a network call out from the view's view model.

As an example, I've created a branch on the MealDBDownloadAsync example discussed in Interacting with Endpoints to take advantage of the already implemented endpoint fetching code.

I've added source code for this article to a branch of the existing for the MealDBDownloadAsync example available on GitHub.

Changes to the View Model

In the previous version of the view model the init function was fetching the data from the network as shown in Listing 1.

init() {
    Task {
        await fetchCategories()
    }
}

Listing 1 - Previous View Model init function

While this is an acceptable approach in this very simple case, if the view model was doing additional initialization it will cause that initialization to be delayed.

We can instead move that Task from the view model to the SwiftUI view code. The new initializer is shown in Listing 2.

init() {
    // Do any additional initialization here that doesn't require network interaction
}

Listing 2 - Updated View Model init function,

Again, because this example is very simple, that leaves our init function empty, but a practical view model would do more initialization here.

Updating the CategoryView

The CategoryView now needs to execute the fetchCategories() function. We'll have it do that in a .task view modifier. This approach doesn't require creating an explicit Task, which having the network fetch in the view model did. The updated CategoryView is shown in Listing 3.

struct CategoryView: View {
    var viewModel = CategoryViewModel()
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(alignment: .leading, spacing: 20.0) {
                    ForEach(viewModel.categories) {category in
                        CategoryRowView(category)
                    }
                }
            }
            .task {
                await viewModel.fetchCategories()
            }
            .padding(.vertical, 20)
        }
        .navigationTitle("Categories")
    }
}

Listing 3 - Updated CategoryView

The only necessary changes were adding lines 12-14 that add the .task view modifier and call the fetchCategories() function through the viewModel.

The .task view modifier is executed only when the ScrollView is displayed, which happens after the CategoryViewModel has already been initialized.

You'll notice that I've only moved the call to the existing fetchCategories() function to the .task view modifier, not the implementation. This helps isolate view code from networking code.

Summary

The .task view modifier is not a replacement for using the .onAppear view modifier to postpone costly initialization until necessary. It is, however, often more appreciate if a portion of that initialization that is asynchronous can be broken out of the view model initialization.

I'd love to hear your comments. Feel free to reach out to me at @sanguish on Mastodon.

sanguish (@sanguish@iosdev.space)
5.89K Posts, 156 Following, 998 Followers · Searching for a macOS or iOS engineering position Ex-Apple DevPubs (12+yrs, remote). I’d go back. macOS/iOS engineer, & author. OCD-sufferer. Like any rational adult, I dislike mimes.

Reach out on Mastodon