Working With The task Modifier In SwiftUI

April 4th, 2025

⏱ Reading Time: 5 mins

It's often necessary to perform asynchronous tasks right when a view appears in SwiftUI. It's common, for instance, to initiate remote data fetching, or load and process data from the local database when a view has loaded and is shown on-screen. One way to do that is to define a Task in the action closure of the onAppear view modifier, and add any asynchronous code there. Although it works, there's a better approach, and that is to use task; a dedicated modifier to run asynchronous code when the view appears.


Preparing the ground for .task

To demonstrate task, we'll use a view that loads fake profiles on appear. To make things as realistic as possible, we'll begin with the following type that represents a profile programmatically:

Besides the few properties, there's also the all static property that returns an array with a few Profile objects to use in the following examples. The last one distinguishes intentionally, as we'll use it later to do some error handling.

Instead of using an actual asynchronous operation, we'll fake one. The next fetchProfile(withID:) static method fetches and returns a matching Profile object from these specified previously. It's defined in another custom type called FakeAsyncService just for the sake of the simulated asynchronous process:

Notice that fetchProfile(withID:) is an asynchronous throwing method. It throws two fake errors just for the demonstration if the asked profile does not exist, or the profile with id value equal to -1 is intentionally requested.

In the view implementation things are simple. If a profile has been loaded then we'll show its details, otherwise a progress indication while fetching the profile. You'll see in the following code that in case of an error we just display it, but that's just to keep things simple. Do not show actual error codes and messages to users in real apps unless it's really necessary, and handle them properly in a user-friendly way:

Using .task

With the above setting the ground, let's use the task modifier to load a profile asynchronously. In its simplest form, all we have to do is to to apply it just like we would do with onAppear, and call the asynchronous operations in it:

See that it's not necessary to initialize a Task block, as task runs the code given in the closure asynchronously. Note that fetchProfile(withID:) is an error-throwing asynchronous method, so we call it with try and await. This particular example requests for the first profile, so this will return the actual data after one second.

Fetching asynchronous data dynamically

The previous task example is good enough if the asynchronous data we are fetching do not depend on any parameters. However, this is not always the case. In our scenario — as it also happens in real-world use cases — different profiles can be fetched based on the profile id we'll pass to the fetchProfile(withID:) method. The sample view we have here could be part of an app showing the profile details once selected among others.

In order to get closer to that actual functionality, let's add one more view. It will present a list with profiles in a navigation stack, and it will navigate to the profile details when selecting one:

Let's update the ProfileDetailView so it can get the profile id as argument:

We'll apply task again, but this time we'll use a different initializer and we'll feed it with an additional argument; the profileID to fetch:

The id parameter can be any value that fits the implementation, as long as it conforms to the Equatable protocol. For clarity, id does not refer to any identifiers related to our models or data by default. It's just a value that SwiftUI observes for changes, executing the task again when they happen. Passing a meaningful value as argument, like the profileID in the previous example, helps us fetch dynamic data asynchronously.

The code inside the task closure does not have to be a single line. Instead, we can apply any logic that fits the needs of the app. For instance, here we could do a better error handling in the throwing fetchProfile(withID:) method, and handle the error case:

Specifying the task priority

By default, an asynchronous task initiated in task modifier has the userInitiated value. We can, however, set a different priority if necessary when initializing the modifier like so:

It's also possible to provide arguments for both the id and the priority parameters:

Tasks are canceled automatically

Any asynchronous task initiated with task is canceled automatically if the view is dismissed before the asynchronous operation is finished. It's easy to find that out, as Swift throws a cancellation error when a task is canceled before it completes. See the following example:

We get started in this task by delaying the execution for a period of time that's sufficient to let us go back to the previous view and dismiss the current one. If we do that within the given time frame, the fetchProfile(withID:) method won't be called. The catch block that handles CancellationError will be executed instead, printing the given message to the console:

If, on the other hand, we don't make the view disappear, the code in the do block will be executed normally and the requested profile will be returned. If you're wondering, the following is taken from the Apple docs:

Cancellation Error
An error that indicates a task was canceled.

https://developer.apple.com/documentation/swift/cancellationerror

Conclusion

The task modifier is the appropriate tool for running asynchronous code when a SwiftUI view appears. We've explored all of its forms in this post through the various initializers and the code examples that we've gone through. If you've been using onAppear and Task blocks, consider switching to task, as it makes it possible to cancel the task in case the view is dismissed while still executing asynchronous code. I hope you've found this tutorial useful. Thanks for reading!


Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.