Skip to content
Prepare to Grow

SwiftUI Fundamentals

Loading Data with task and Async/Await

Connect Swift concurrency to the view lifecycle with task, explicit loading states, cancellation, retry, and refresh behavior.

Intermediate6 min read2 guided activities

Before you begin

What you will understand

  • Start lifecycle-aware asynchronous work with task.
  • Model loading, content, empty, and failure states explicitly.
  • Respond to changing inputs with task(id:).
  • Handle cancellation and retry without stale updates.
01

Learning stage

Concept Explanation

The task modifier starts asynchronous work associated with a view's lifetime. SwiftUI can cancel that task when the view disappears or when a task(id:) identifier changes. This is usually a better lifecycle match than starting untracked work from body or init.

Asynchronous UI needs an explicit state model. Loading, content, empty, and failure are different user experiences, not only an optional array and a Boolean. An enum prevents contradictory states and gives each branch the information it requires.

Cancellation is expected control flow. Let cancellation propagate, check it during long work, and avoid replacing useful content with a cancellation error. For input-driven work such as search, task(id:) cancels the prior task as the ID changes; a small delay can provide cooperative debouncing.

  • task: starts async work tied to view lifetime.
  • task(id:): restarts work when an Equatable input changes.
  • LoadState: one enum representing mutually exclusive UI states.
  • Cancellation: cooperative end of obsolete work.
  • refreshable: exposes a platform refresh action backed by async work.
02

Learning stage

Syntax & Examples

The screen owns a model that exposes one complete load state and uses task for lifecycle-aware loading and retry.

CourseCatalog.swiftswift
import Observation
import SwiftUI

enum LoadState<Value> {
  case idle
  case loading
  case loaded(Value)
  case failed(message: String)
}

@MainActor @Observable
final class CatalogModel {
  var state: LoadState<[Course]> = .idle
  private let client: CourseClient

  init(client: CourseClient) { self.client = client }

  func load() async {
    if case .loading = state { return }
    state = .loading
    do {
      let courses = try await client.fetchCourses()
      try Task.checkCancellation()
      state = .loaded(courses)
    } catch is CancellationError {
      return
    } catch {
      state = .failed(message: "Courses could not be loaded.")
    }
  }
}

struct CatalogScreen: View {
  @State var model: CatalogModel

  var body: some View {
    CatalogContent(state: model.state, retry: { Task { await model.load() } })
      .task { await model.load() }
      .refreshable { await model.load() }
  }
}
The async operation is tied to the screen lifecycle, and a single enum ensures the view renders one valid loading state at a time.
03

Learning stage

Real-World Usage

Connect the SwiftUI concept to complete, maintainable application features.

  • Load remote or local data when a screen becomes active.
  • Refresh content with the system refresh gesture.
  • Restart search or detail loading when an input ID changes.
  • Keep previous content visible while performing a background refresh when product behavior calls for it.
04

Learning stage

Common Mistakes

Identify patterns that weaken ownership, identity, adaptation, accessibility, or user experience.

  • Starting Task inside body.
  • Using onAppear repeatedly without guarding duplicate work.
  • Representing loading, error, and content with contradictory independent flags.
  • Displaying cancellation as a user-facing failure.
  • Allowing an older request to overwrite a newer search result.
  • Capturing a service implicitly instead of injecting a controllable dependency.
05

Learning stage

Practice Exercises

Build the behavior in a small Xcode project and review it in multiple states.

Activity 1

Build a complete loading screen

Create an enum for idle, loading, loaded, empty, and failed. Render a distinct accessible UI for each and add retry.

Hint: If an empty array has a different meaning from loaded content, represent that meaning deliberately.

Activity 2

Create cancellable search

Use task(id:) with search text, a short clock delay, and cancellation to prevent stale results.

Hint: After every suspension point, consider whether the task may have become obsolete.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What important behavior does the task modifier provide?

  1. A.It associates asynchronous work with the view lifecycle
  2. B.It makes all work synchronous
  3. C.It stores data permanently
  4. D.It disables cancellation
Reveal answer

It associates asynchronous work with the view lifecycle

SwiftUI starts the task for the view and can cancel it when its lifecycle or task ID changes.

QUESTION 2

Should a cancelled search normally show a failure alert?

Reveal answer

No. Cancellation usually means the result became obsolete, not that the user experienced a failure.

Treat expected cancellation separately from errors that require recovery or explanation.

07

Learning stage

Interview Preparation

Explain the design in terms of data flow, identity, lifecycle, platform behavior, and trade-offs.

How would you structure a SwiftUI screen that loads remote data?

Short answer: Use an injected async client, a main-actor observable model with one load-state enum, and a task tied to the view lifecycle.

Explore the detailed answer

The model performs the request and publishes one valid state. The view renders loading, content, empty, or failure branches and exposes retry. task starts work when appropriate, cancellation is treated as expected control flow, and request identity prevents stale results from winning.

Possible follow-ups

  • • When would you use task(id:)?
  • • How would you keep old content during refresh?
08

Learning stage

Key Takeaways

Lifecycle-aware async UI requires more than await. Tie work to view identity, represent complete user-facing states, treat cancellation as normal, and prevent obsolete tasks from updating current data.

  • task connects async work to a view lifecycle.
  • task(id:) restarts work when input changes.
  • Use one explicit state model for mutually exclusive UI states.
  • Cancellation is not necessarily an error.
  • Inject async dependencies for testing and control.