SwiftUI .task() vs .onAppear()

Aug 23, 2024#swiftui

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)
    }
  }
}