SwiftUI .task() vs .onAppear()

Apr 22, 2023#swiftui#swift#ios

In SwiftUI, both .task() and .onAppear() are view modifiers that can be used to run some code when a view is shown. The exact moment that SwiftUI calls these methods depends on the specific view type that you apply to, but the action closures complete before the first rendered frame appears.

// iOS 13.0+, macOS 10.15+, watchOS 6.0+
func onAppear(perform action: (() -> Void)? = nil) -> some View

// iOS 15.0+, macOS 12.0+, watchOS 8.0+
func task(
    priority: TaskPriority = .userInitiated,
    _ action: @escaping () async -> Void
) -> some View

// iOS 15.0+, macOS 12.0+, watchOS 8.0+
func task<T>(
    id value: T,
    priority: TaskPriority = .userInitiated,
    _ action: @escaping () async -> Void
) -> some View where T : Equatable

In general, .task() is a more powerful and convenient modifier than .onAppear() for running asynchronous work when a view is shown. However, it is only available in iOS 15+, so you may need to use .onAppear() for backward compatibility. You can always use .onAppear() with Task to replicate what .task() does.

There are some differences between them:

  1. Newer modifer .task() allows you to use the async/await directly to perform asynchronous work. While .onAppear() is an older modifier that can only run synchronous code, you need to use Task or DispatchQueue to bridge to async code.
struct ContentView: View {
    @State var text: String = "Loading..."

    // Async function
    func asyncGetText() async -> String {
        // Simulate some network request
        await Task.sleep(3_000_000_000)
        return "Hello, world!"
    }

    var body: some View {
        Text(text)
            .padding()
            // Start a task when the view is shown
            .task {
                // Await the result of the async function
                let result = await asyncGetText()
                // Update the state with the result
                text = result
            }
    }
}

Similar example but using .onAppear():

struct ContentView: View {
    /* ... */

    var body: some View {
        Text(text)
            .padding()
            // Run some code when the view is shown
            .onAppear {
                // Start a task to call the async function
                Task {
                    // Await the result of the async function
                    let result = await asyncGetText()
                    // Update the state with the result
                    text = result
                }
            }
    }
}
  1. Modifer .task() will automatically cancel the task when the view is destroyed, if it has not already finished. .onAppear() does not have this feature, so you may need to manually cancel any ongoing work in .onDisappear().
struct ContentView: View {
    /* ... */
    private var task: Task<Void, Never>? // reference to the task

    var body: some View {
        Text(text)
            .padding()
            // Run some code when the view is shown
            .onAppear {
                // Start a task to call the async function
                task = Task {
                    // Await the result of the async function
                    let result = await asyncGetText()
                    // Update the state with the result
                    text = result
                }
            }
            // Run some code when the view is hidden
            .onDisappear {
                // Cancel the task if it is not finished
                task?.cancel()
            }
    }
}
  1. Modifier .task() can take an id parameter that allows you to cancel and restart the task when the value changes. For example, you can use this to listen to notifications or update data based on some state. .onAppear() does not have this feature, so you may need to use @State or @Binding variables to trigger updates.
struct ContentView: View {
    @State var text: String = "Loading..."
    @State var category: String = "Swift" // the id for the task

    // Async function
    func asyncGetText(category: String) async -> String {
        // Simulate some network request
        await Task.sleep(3_000_000_000)
        return "Hello, \(category)!"
    }

    var body: some View {
        VStack {
            Text(text)
                .padding()
                // Start a task with an id when the view is shown
                .task(id: category) {
                    // Await the result of the async function
                    let result = await asyncGetText(category: category)
                    // Update the state with the result
                    text = result
                }
            // A picker to change the category
            Picker("Select a category", selection: $category) {
                Text("Swift").tag("Swift")
                Text("iOS").tag("iOS")
                Text("macOS").tag("macOS")
            }
            .pickerStyle(.segmented)
        }
    }
}
  1. Modifier .task() can also take a priority parameter that allows you to specify the priority of the task relative to other tasks. For example, you can use this to prioritize user-initiated tasks over background tasks. .onAppear() does not have this feature, so you may need to use Task, DispatchQueue or OperationQueue to manage priorities.
struct ContentView: View {
    @State var text: String = "Loading..."
    @State var priority: TaskPriority = .userInitiated // the priority for the task

    // Async function
    func asyncGetText(priority: TaskPriority) async -> String {
        // Simulate some network request
        await Task.sleep(3_000_000_000)
        return "Hello, \(priority)!"
    }

    var body: some View {
        VStack {
            Text(text)
                .padding()
                // Start a task with a priority when the view is shown
                .task(priority: priority) {
                    // Await the result of the async function
                    let result = await asyncGetText(priority: priority)
                    // Update the state with the result
                    text = result
                }
            // A picker to change the priority
            Picker("Select a priority", selection: $priority) {
                Text("User Initiated").tag(TaskPriority.userInitiated)
                Text("High").tag(TaskPriority.high)
                Text("Medium").tag(TaskPriority.medium)
                Text("Low").tag(TaskPriority.low)
                Text("Background").tag(TaskPriority.background)
            }
            .pickerStyle(.segmented)
        }
    }
}