SwiftUI task modifiers and async lifecycle

Sofia Martinez Jan 2026
1 tab
import SwiftUI

struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()

    var body: some View {
        List(viewModel.posts) { post in
            PostRowView(post: post)
        }
        .task {
            await viewModel.loadPosts()
        }
        .refreshable {
            await viewModel.refresh()
        }
    }
}

// Search with task(id:)
struct SearchView: View {
    @State private var searchQuery = ""
    @State private var results: [SearchResult] = []
    @State private var isLoading = false

    var body: some View {
        VStack {
            TextField("Search", text: $searchQuery)
                .textFieldStyle(.roundedBorder)
                .padding()

            if isLoading {
                ProgressView()
            } else {
                List(results) { result in
                    Text(result.title)
                }
            }
        }
        .task(id: searchQuery) {
            guard !searchQuery.isEmpty else {
                results = []
                return
            }

            isLoading = true

            // Debounce
            try? await Task.sleep(nanoseconds: 300_000_000)

            // Check if task was cancelled
            guard !Task.isCancelled else { return }

            do {
                results = try await searchAPI(query: searchQuery)
            } catch {
                print("Search failed: \(error)")
            }

            isLoading = false
        }
    }

    private func searchAPI(query: String) async throws -> [SearchResult] {
        // Simulate API call
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return []
    }
}

// Parallel async operations
struct DashboardView: View {
    @State private var posts: [Post] = []
    @State private var comments: [Comment] = []
    @State private var users: [User] = []
    @State private var isLoading = false

    var body: some View {
        VStack {
            if isLoading {
                ProgressView("Loading...")
            } else {
                Text("\(posts.count) posts")
                Text("\(comments.count) comments")
                Text("\(users.count) users")
            }
        }
        .task {
            isLoading = true

            // Load all data in parallel
            async let postsData = fetchPosts()
            async let commentsData = fetchComments()
            async let usersData = fetchUsers()

            do {
                posts = try await postsData
                comments = try await commentsData
                users = try await usersData
            } catch {
                print("Error loading data: \(error)")
            }

            isLoading = false
        }
    }

    private func fetchPosts() async throws -> [Post] {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return []
    }

    private func fetchComments() async throws -> [Comment] {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return []
    }

    private func fetchUsers() async throws -> [User] {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return []
    }
}

// Manual task management
struct VideoPlayerView: View {
    @State private var downloadTask: Task<Void, Never>?
    @State private var downloadProgress: Double = 0

    var body: some View {
        VStack {
            if let task = downloadTask, !task.isCancelled {
                ProgressView(value: downloadProgress)
                Button("Cancel Download") {
                    task.cancel()
                    downloadTask = nil
                }
            } else {
                Button("Download Video") {
                    startDownload()
                }
            }
        }
    }

    private func startDownload() {
        downloadTask = Task {
            for i in 1...100 {
                guard !Task.isCancelled else { return }

                try? await Task.sleep(nanoseconds: 50_000_000)
                downloadProgress = Double(i) / 100.0
            }
        }
    }
}
1 file · swift Explain with highlit

SwiftUI's .task modifier handles async operations tied to view lifecycle. The task starts when the view appears and cancels automatically when it disappears, preventing memory leaks. I use .task for data fetching, subscriptions, and long-running operations. For manual control, I track task handles with @State var task: Task<Void, Never>? and cancel explicitly. The .task(id:) variant re-runs when dependencies change, perfect for search queries. Structured concurrency with async let enables parallel operations. Task priorities guide scheduling—.userInitiated for UI-critical work, .background for non-urgent tasks. .refreshable creates pull-to-refresh with async support.