Skip to main content

How to create and run a task

About 5 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftxcodeappstore

How to create and run a task 관련

Swift Concurrency by Example

Back to Home

How to create and run a task | Swift Concurrency by Example

How to create and run a task

Updated for Xcode 15

Swift’s Task struct lets us start running some work immediately, and optionally wait for the result to be returned. And it is optional: sometimes you don’t care about the result of the task, or sometimes the task automatically updates some external value when it completes, so you can just use them as “fire and forget” operations if you need to. This makes them a great way to run async code from a synchronous function.

First, let’s look at an example where we create two tasks back to back, then wait for them both to complete. This will fetch data from two different URLs, decode them into two different structs, then print a summary of the results, all to simulate a user starting up a game – what are the latest news updates, and what are the current highest scores?

Here’s how that looks:

struct NewsItem: Decodable {
    let id: Int
    let title: String
    let url: URL
}

struct HighScore: Decodable {
    let name: String
    let score: Int
}

func fetchUpdates() async {
    let newsTask = Task { () -> [NewsItem] in
        let url = URL(string: "https://hws.dev/headlines.json")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([NewsItem].self, from: data)
    }

    let highScoreTask = Task { () -> [HighScore] in
        let url = URL(string: "https://hws.dev/scores.json")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([HighScore].self, from: data)
    }

    do {
        let news = try await newsTask.value
        let highScores = try await highScoreTask.value
        print("Latest news loaded with \(news.count) items.")

        if let topScore = highScores.first {
            print("\(topScore.name) has the highest score with \(topScore.score), out of \(highScores.count) total results.")
        }
    } catch {
        print("There was an error loading user data.")
    }
}

await fetchUpdates()

Download this as an Xcode projectopen in new window

Let’s unpick the key parts:

  1. Creating and running a task is done by using its initializer, passing in the work you want to do.
  2. Tasks don’t always need to return a value, but when they do chances are you’ll need to declare exactly what as you create the task – I’ve said () -> [NewsItem] in, for example.
  3. As soon as you create the task it will start running – there’s no start() method or similar.
  4. The entire task is run concurrently with your other code, which means it might be able to run in parallel too. In our case, that means fetching and decoding the data happens inside the task, which keeps our main fetchUpdates() function free.
  5. If you want to read the return value of a task, you need to access its value property using await. In our case our task could also throw errors because we’re accessing the network, so we need to use try as well.
  6. Once you’ve copied out the value from your task you can use that normally without needing await or try again, although subsequent accesses to the task itself – e.g. newsTask.valuewill need try await because Swift can’t statically determine that the value is already present.

Both tasks in that example returned a value, but that’s not a requirement – the “fire and forget” approach allows us to create a task without storing it, and Swift will ensure it runs until completion correctly.

To demonstrate this, we could make a small SwiftUI program to fetch a user’s inbox when a button is pressed. Button actions are not async functions, so we need to launch a new task inside the action. The task can call async functions, but in this instance we don’t actually care about the result so we’re not going to store the task – the function it calls will handle updating our SwiftUI view.

Here’s the code:

struct Message: Decodable, Identifiable {
    let id: Int
    let from: String
    let text: String
}

struct ContentView: View {
    @State private var messages = [Message]()

    var body: some View {
        NavigationView {
            Group {
                if messages.isEmpty {
                    Button("Load Messages") {
                        Task {
                            await loadMessages()
                        }
                    }
                } else {
                    List(messages) { message in
                        VStack(alignment: .leading) {
                            Text(message.from)
                                .font(.headline)

                            Text(message.text)
                        }
                    }
                }
            }
            .navigationTitle("Inbox")
        }
    }

    func loadMessages() async {
        do {
            let url = URL(string: "https://hws.dev/messages.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            messages = try JSONDecoder().decode([Message].self, from: data)
        } catch {
            messages = [
                Message(id: 0, from: "Failed to load inbox.", text: "Please try again later.")
            ]
        }
    }
}

Download this as an Xcode projectopen in new window

Even though that code isn’t so different from the previous example, I still want to pick out a few things:

  1. Creating the new task is what allows us to start calling an async function even though the button’s action is a synchronous function.
  2. The lifetime of the task is not bound by the button’s action closure. So, even though the closure will finish immediately, the task it created will carry on running to completion.
  3. We aren’t trying to read a return value from the task, or storing it anywhere. This task doesn’t actually return anything, and doesn’t need to.

I know it’s a not a lot of code, but between Task, async/await, and SwiftUI a lot of work is happening on our behalf. Remember, when we use await we’re signaling a potential suspension point, and when our functions resume they might be on the same thread as before or they might not.

In this case there are potentially four thread swaps happening in our code:

  • All UI work runs on the main thread, so the button’s action closure will fire on the main thread.
  • Although we create the task on the main thread, the work we pass to it will execute on a background thread.
  • Inside loadMessages() we use await to load our URL data, and when that resumes we have another potential thread switch – we might be on the same background thread as before, or on a different background thread.
  • Finally, the messages property uses the @State property wrapper, which will automatically update its value on the main thread. So, even though we assign to it on a background thread, the actual update will get silently pushed back to the main thread.

Best of all, we don’t have to care about this – we don’t need to know how the system is balancing the threads, or even that the threads exist, because Swift and SwiftUI take care of that for us. In fact, the concept of tasks is so thoroughly baked into SwiftUI that there’s a dedicated task() modifier that makes them even easier to use.

Similar solutions…
What’s the difference between a task and a detached task? | Swift Concurrency by Example

What’s the difference between a task and a detached task?
How to run tasks using SwiftUI’s task() modifier | Swift Concurrency by Example

How to run tasks using SwiftUI’s task() modifier
How to create and use task local values | Swift Concurrency by Example

How to create and use task local values
What’s the difference between async let, tasks, and task groups? | Swift Concurrency by Example

What’s the difference between async let, tasks, and task groups?
How to cancel a Task | Swift Concurrency by Example

How to cancel a Task

이찬희 (MarkiiimarK)
Never Stop Learning.