Use Async / Await in SwiftUI
Tags: swift, swiftu, async, await
Published: 2022-12-05 16:33:31

Convert your SwiftUI app to take advantage of Swift's new multithreading mechanism and see what's going on underneath the colorful shell.

> **Software version**
> Swift 5.5, iOS 15, Xcode 13

Swift 5.5 has a bright new `threading framework` to help you write safe code faster. To help everyone get started, Apple provided a bunch of videos and code examples at WWDC 2021. There is a summary of what they cover at the end of this tutorial.

Twitter has exploded, and the usual actors (actors - this concept will be discussed later in the article) have already published several instructions. This tutorial is similar to the micro version of [Swift concurrency: Update a sample app](https://developer.apple.com/wwdc21/10194) from WWDC. You'll take small steps to transform a much simpler application to learn how async/await and actors help you write safer code. To help you decipher Xcode error messages and protect you from inevitable future API changes, you'll learn what's going on under the bright surface.

> **The note**
> You will need Xcode 13. This tutorial was written using beta 1. If you want to run it on an iOS device, Xcode 13 must be running iOS 15 beta. For your Mac, Big Sur will do. If you have a Mac [partition] running the Monterey beta, you can try running your code there if it doesn't work in Big Sur. You should be comfortable using SwiftUI, Swift, and Xcode to develop iOS apps.

## Let's get started
Create a new Xcode project using the SwiftUI interface and name it WaitForIt.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5l5e49evawl23j9hdrvl.png)

In `ContentView.swift`, replace the body content with this code:

```
AsyncImage(url: URL(string: "https://files.betamax.raywenderlich.com/attachments/collections/194/e12e2e16-8e69-432c-9956-b0e40eb76660.png")) { image in
  image.resizable()
} placeholder: {
  Color.red
}
.frame(width: 128, height: 128)
```
In Xcode 13 beta 1 you get this error:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9189gv54ro4m2js1b9hd.png)

**Do not click** any of the **Fix **buttons! Go to the landing page and change the **Deployment Info** from `iOS 14.0` to `iOS 15.0`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y0pxuzyye8umjj1w6br6.png)

Return to `ContentView.swift`. If the error message is still present, press **Command-B** to build the project.

Launch **Live Preview** to see the image for the video “SwiftUI vs. UIKit”:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g5hw84y9y6hdt98ymlt0.png)

Okay, this was just a quick check to fix this Xcode crash and also show you the new SwiftUI `AsyncImage ` view. Okay, right?

Before you get started on a real WaitForIt application, take a close look at how Swift's new threading fixes problems with the old GCD threading.

## Old and new multithreading
The old GCD threading has several problems that make it difficult to write applications that use threading safely.

Multithreading Swift provides the necessary tools to split work into smaller tasks that can run concurrently. This allows tasks to wait for each other to complete and allows you to efficiently manage the overall progress of a task.

## Pyramid of Doom (Pyramid of Doom)
Swift APIs like URLSession are asynchronous. Methods are automatically dispatched to a background queue and immediately return control to the calling code. The methods take a completion handler and call delegate methods. Finalization or delegation code that accesses UI elements must be sent to the main queue.

If a completion handler calls another asynchronous function, and that function has a completion handler, it's hard to see the exit in the resulting pyramid of doom. This makes it difficult to check the correctness of the code. For example, this code example from [Meet async/await in Swift](https://developer.apple.com/wwdc21/10132) at WWDC loads data, creates an image from the data, and then displays a thumbnail of the image. Error handling is special because completion handlers cannot throw errors.

```
func fetchThumbnail(
  for id: String,
  completion: @escaping (UIImage?, Error?) -> Void
) {
  let request = thumbnailURLRequest(for: id)
  let task = URLSession.shared
    .dataTask(with: request) { data, response, error in
    if let error = error {
      completion(nil, error)
    } else if (response as? HTTPURLResponse)?.statusCode != 200 {
      completion(nil, FetchError.badID)
    } else {
      guard let image = UIImage(data: data!) else {
        completion(nil, FetchError.badImage)
        return
      }
      image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
        guard let thumbnail = thumbnail else {
          completion(nil, FetchError.badImage)
          return
        }
        completion(thumbnail, nil)
      }
    }
  }
  task.resume()
}
```
The sequence of operations is much easier to see with async/await, and you can take advantage of Swift's robust error handling mechanism:

```
func fetchThumbnail(for id: String) async throws -> UIImage {
  let request = thumbnailURLRequest(for: id)
  let (data, response) = try await URLSession.shared.data(for: request)
  guard (response as? HTTPURLResponse)?.statusCode == 200 else {
    throw FetchError.badID
  }
  let maybeImage = UIImage(data: data)
  guard let thumbnail = await maybeImage?.thumbnail else {
    throw FetchError.badImage
  }
  return thumbnail
}
```

## Data Races
When multiple tasks can read or write object data, data races are possible. A data race occurs when one task pauses while another task writes and exits, then the sleeping task resumes and overwrites what was written by the previous task. This creates inconsistent results.

In an app using legacy multithreading, Xcode can detect data races if you enable the `Thread Sanitizer` runtime diagnostic in your app's `Run `scheme. You can then implement a sequential queue to prevent concurrent access.

Swift's new multithreading model provides the Actor protocol to prevent concurrent access to object data. Subjects also allow you to structure your application into code that runs on the main thread and code that runs on background threads, so the compiler can help you prevent concurrent access.

## Thread Explosion/Starvation (stream explosion/starvation)
In GCD, the basic unit of work is the `thread`. If your code queues a lot of read/write tasks, most of them should sleep while they wait. This means that their threads are blocked, so the system creates more threads for the next tasks. If each task also puts a completion handler on a different queue, this creates even more threads. Each blocked thread holds the stack and kernel data structures so that it can be resumed. A blocked thread may be holding resources that another thread needs, so the thread is blocked.

This is an explosion of threads: the system is overloaded with many times more threads than there are cores to process them. The scheduler has to allocate time to hundreds of threads, which results in a lot of context switches. All this slows down your application and may even starve some threads so they never run.

## Tasks and continuations
In Swift multithreading, the basic unit of work is the task. A task executes tasks sequentially. To achieve multithreading, a task can create child tasks. Or you can create tasks in a task group.

The system knows that these tasks are related, so it can manage due dates, priority, and cancellation flags for all tasks in a task tree or group. This makes it easier to check for and respond to cancellation status, thus avoiding issue leaks. If it is important to respond immediately to cancellation, you can write a function with a cancellation handler.

If a task is suspended, it releases its thread and saves its state in the `continuation`. Threads switch between continuations instead of context switching.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnt6ib2748fhbdkp005y.png)

Threads switch between continuations.

> **The note**
> This image is taken from the WWDC session of [Swift concurrency: Behind the scenes](https://developer.apple.com/wwdc21/10254)
> The `await `keyword marks the point of suspension, and the `async frame` on the heap stores the information it needs when it resumes.

Ideally, the number of threads never exceeds the number of cores. There is a shared pool of threads and a runtime contract whereby each thread will do its job. Your code maintains this contract by using await, actors, and task groups to make dependencies visible to the compiler.

## Joke Service
Enough theory! It's time to convert simple loading to using async/await.

The starter folder contains `JokeService.swift`. Add this file to WaitForIt.

`JokeService` is an `ObservableObject `that sends a request to an API that returns a random [Chuck Norris](https://en.wikipedia.org/wiki/Chuck_Norris) joke. I adapted this code from the sample application in [Combine: Asynchronous Programming with Swift](https://www.raywenderlich.com/books/combine-asynchronous-programming-with-swift). The request element specifies the category of developers, so all the jokes have a technical flavor. Warning: Some of these jokes are a little cruel.

The `JokeService `publishes the joke and its `isFetching `status. Its `fetchJoke()` method uses the standard `URLSession.shared.dataTask` with a completion handler. If something goes wrong, it prints an error message with either a `dataTask `error or an "Unknown error". In the case of "Unknown error", it does not provide information on whether the problem was in the data or in the decoder.

## Minimal error handling
Reliable error handling is one of the main reasons for using async/await. The data task completion handler cannot throw errors, so if it calls a generating function such as `JSONDecoder().decode(_:from:)`, it must handle any errors that are thrown.

Usually they take the easy way out and just ignore the error. This is what the starter file does:

`if let decodedResponse = try? JSONDecoder().decode(Joke.self, from: data)`

Previous versions of Xcode offer this as a fix if you just write try and don't wrap it in do/catch. This means: just assign `nil `if the function throws an error.

Delete ? to see what's going on:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxune11cytvuazg6qntu.png)

Xcode is now taking a tougher stance: no more helpful suggestions for simple fixes.

But ? still works here so bring it back.

## Show me a joke!
To get the joke, open `ContentView.swift` and replace the content of the `ContentView `with this:

```
@StateObject var jokeService = JokeService()

var body: some View {
  ZStack {
    Text(jokeService.joke)
      .multilineTextAlignment(.center)
      .padding(.horizontal)
    VStack {
      Spacer()
      Button { jokeService.fetchJoke() } label: {
        Text("Fetch a joke")
          .padding(.bottom)
          .opacity(jokeService.isFetching ? 0 : 1)
          .overlay {
            if jokeService.isFetching { ProgressView() }
          }
      }
    }
  }
}
```
Launch **Live Preview** and click the button. This has a nice effect with opacity and `ProgressView()` to indicate that a fetch is in progress.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9nwwtfugvaa47uar2gl.png)

## Concurrent Binding (parallel binding)
Okay, the old way works, so now you're going to convert it to the new one.

Comment out `URLSession` up to and including `.resume()`.

Add this code below `isFetching = true`

```
async let (data, response) = URLSession.shared.data(from: url)
```
The new `data(from:) URLSession` method is asynchronous, so you use `async let` to assign the return value to the `(data, response)` tuple. These are the same `data `and `response `that `dataTask(with:)` provides to its completion handler, but `data(from:)` returns them directly to the calling function.

Where is the error that `dataTask(with:)` throws? You'll find out soon - stay tuned!

The following errors and suggested fixes appear:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g88r5k79knibqf3ca18l.png)

The errors are similar: you can't call an async function within a synchronous function. You must tell the compiler that `fetchJoke()` is asynchronous.

Both fixes are the same, so click on either one. This gives you:

```
func fetchJoke() async {
```
Like `throws`, the `async `keyword appears between the closing brace and the opening curly brace. You will interrupt `throws `again soon.

Back to `async let:` this is one way to assign the result of `data(from:)` to the tuple `(data, response)`. This is called **concurrent binding** because the parent task continues executing after the child task is created to run `data(from:)` on a different thread. The child task inherits the priority and local values ​​of the parent task. When the parent task needs to use `data `or `response`, it suspends itself (releases its thread) until the child task completes.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfcgf9lt0uv9zrdsrs56.png)

Parent and child tasks run at the same time.

## async wait
The verb for `async `is `await`, just like the verb for `throws `is `try`. You try the throwing function and wait for the async function.

Add this line of code:

```
await (data, response)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rzk4vidqm61t042ht0f.png)

And there's a missing error here that `dataTask(with:)` passes to its completion handler: `data(from:)` throws it. So, you should use `try await`:

```
try! await (data, response)
```

> **The note**
> The keywords must be in that order, **not **`await try`.

You're not really going to use this code, so don't bother catching bugs. It's just a chance to see what's going on.

What is happening is amazing:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vicbnkbpw6crhwhkl5mu.png)

An immutable value can only be initialized once.

This is surprising because the [Explore structured concurrency in Swift](https://developer.apple.com/wwdc21/10134) video says, “And don't worry. Reading the result value again will not recalculate its value."

> **The note**
> Looks like it's a tuple error. You can wait for data or a response, but not both.

Go ahead and accept the suggested fix to change `let `to `var`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6defovon7i28zvpgim4l.png)

Hm! Think back to your early days learning Swift, when Xcode kept saying, "You can't do that here." Maybe it's a beta bug. It doesn't matter in this case, because between calling `data(from:)` and processing what it returns, there is no other code to execute.

## Sequential Binding (serial binding)
Instead, you will use another binding: **sequential binding**.

Replace two lines with this piece of code:

`let (data, response) = try? await URLSession.shared.data(from: url)`

Unlike `async let`, calling `data(from:)` in this way does not create a child task. It is executed sequentially as a job in the `fetchJoke()` task. While it waits for a response from the server, this job suspends itself, releasing the task thread.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gu0rkvgqfz3ypb66ocf4.png)

The `data(from:)` tasks are suspended.

But there's a problem:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6p2oed9p3xj3j9oklsny.png)

Xcode refuses to understand `try?` here.

You'll try the lazy way, but this time it won't work in Xcode, even if you use nil union to specify a nil tuple:

```
let (data, response) = 
  try? await URLSession.shared.data(from: url) ?? (nil, nil)
```

No, you have to do the right thing. First, remove the ? and ?? (nil, nil):

```
let (data, response) = try await URLSession.shared.data(from: url)
```

## Error handling options
You have two options for handling errors caused by `data(from:)`. The first is to bite the bullet and deal with it right away with do/catch:

```
do {
  let (data, response) = try await URLSession.shared.data(from: url)
} catch {
  print(error.localizedDescription)
}
```
An easier(?) option is to make `fetchJoke()` throw:

```
func fetchJoke() async throws {
```

These keywords must be in the order listed - `throws async` does not work:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vzquvp8sex8arbhowfr7.png)

`async `must come before `throws`.

Now `fetchJoke()` simply propagates the error to everyone who calls `fetchJoke()`. This is the button in the ContentView, where Xcode is already complaining about `fetchJoke()` being asynchronous:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqrenw0z3sfd5rw6kpat.png)

`fetchJoke()` is `async `and returns: Do something!

So what's now? You cannot mark anything in a `ContentView `as `async`.

## Create an unstructured task
Luckily, you can create an asynchronous task in a button action. Replace Button { jokeService.fetchJoke() } label: { with this piece of code:

```
Button {
  async {
    try? await jokeService.fetchJoke()
  }
} label: {
```

You create an asynchronous task with `async { }`. Since it is asynchronous, you need to wait for it to complete. Because it `throws`, you should try to catch any errors. Xcode allows you to use `try?` here, or you can write a do/catch statement.

> **The note**
> The syntax for creating a task will change to `Task { ... }` in a future beta release.

This is an **unstructured task** because it is not part of the **task tree**. The `async let` task that you created in `fetchJokes()` is a **child task** of the task that `fetchJokes()` runs on. A child task is bound to the scope of its parent task: the fetchJokes() task cannot complete until its child tasks have completed.

An unstructured task inherits the actor, priority, and local values ​​of its origin, but is not limited in scope. Canceling the original task does not signal an unstructured task, and the original task can be completed even if the raw task is not completed.

Creating an unstructured task in a non-async context looks just like `DispatchQueue.global().async`, only with less input. But there is a big difference: it runs on the `MainActor `thread with `userInteractive `priority when the main thread is not blocked.

You can specify a lower priority with `asyncDetached `:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnwtst9s7lk0gkgn4qx1.png)

Specify a priority for an individual task.

But it will still run on the main thread. More on this later.

## Joke decoding
Return to `JokeService.swift` to finish writing `fetchJoke()`. If you thought throwing was the easier option, let's see what you think after this section.

Since `fetchJoke()` throws, it passes on any error thrown by `data(from:)` to the calling function. You can also take advantage of this mechanism and throw other errors that may occur.

Errors thrown by the calling function must comply with the `Error `protocol, so add this code above the `JokeService `extension:

```
enum DownloadError: Error {
  case statusNotOk
  case decoderError
}
```

You are creating an enumeration of the possible errors that `fetchJoke()` might throw.

Then add this piece of code to `fetchJoke()`:

```
guard 
  let httpResponse = response as? HTTPURLResponse,
  httpResponse.statusCode == 200   // 1
else {
  throw DownloadError.statusNotOk
}
guard let decodedResponse = try? JSONDecoder()
  .decode(Joke.self, from: data) // 2
else { throw DownloadError.decoderError }
joke = decodedResponse.value   // 3
```

Using `guard `allows you to pass your specific errors to the calling function.

1. You check the response status code and throw `statusNotOk `if it's not 200.

2. You decode the response and throw a `decoderError `if something goes wrong.

3. You assign the decoded `joke `value.

> **The note**
> You always have the option of catching an error, including those caused by `data(from:)`, instead of throwing one.

Now where to set `isFetching `to `false`? This `Published `value controls the button's `ProgressView`, so you want to set it even if `fetchJoke() `throws an error. Throwing an error causes `fetchJokes()` to exit, so you still need to set `isFetching `in the defer statement before any possible early exit.

Add this line right below `isFetching = true;`

```
defer { isFetching = false }
```

## MainActor
If Xcode has tweaked you a lot, you might feel a little uncomfortable. `Published `values ​​update SwiftUI views, so you can't set `Published `values ​​from a background thread. To set `Published `to `isFetching `and `joke`, a `dataTask(with:)` completion handler is dispatched to the main queue. But your new code doesn't bother to do that. Will you get main thread errors when you run the application?

Try it. Build and run in the simulator. No, there are no mainstream errors. Why not?

Because you used `async { }` to create the `fetchJoke()` task in the button action, it's already running on the `MainActor `thread with UI priority.

`Actor` is a Swift multithreading mechanism that allows you to make an object thread-safe (thread-safe). Like `Class`, it is a named reference type. Its synchronization mechanism isolates its shared mutable state and does not guarantee concurrent access to that state.

MainActor is a special Actor that represents the main thread. You can think of this as just using `DispatchQueue.main`. All SwiftUI views run on the `MainActor `thread, just like the flat task you created.

To see this, place a breakpoint anywhere in `fetchJoke()`. Make a build and run, then click the button.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xphjahs7dhouss5c3r2k.png)

`fetchJoke() `runs on the main thread.

Yes, `fetchJoke()` runs on the main thread.

What if you lower the priority? In **ContentView.swift** in button action change `async {` to this:

```
asyncDetached(priority: .default) {
```

> **The note**
> The syntax for this parameter will change to `Task.detached` in a future beta release.

Build and run. Click on the button:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wl0ixmsa6w7r9n06jqai.png)

`fetchJoke()` is still running on the main thread.

You lowered the priority to the default, but this does not move the task to the background queue. The task is still running on the main thread!

> **The note**
> It looks like it's a coincidence. The video [Explore structured concurrency in Swift](https://developer.apple.com/wwdc21/10134) says that a detached task doesn't inherit anything from its source, so it shouldn't inherit from the MainActor thread. A future beta release of Xcode may provide this.

Change the code back to `async {`.

To move asynchronous work from the main thread, you need to create an `actor `that is not `MainActor`.

## Actor
Actors allow you to structure your application into actors on background threads and actors on the main thread, just like you now create a model, view and view model files. The code in actor (lowercase, not MainActor) is running on a background thread. So you just need to move the async part of fetchJoke() to a separate actor.

In `JokeService.swift`, remove the breakpoint and add this piece of code above the `JokeService`:

```
private actor JokeServiceStore {
  private var loadedJoke = Joke(value: "")
  
  func load() async throws -> Joke {
  }
}
```

You create an `actor `with a `Joke `variable and initialize it to an empty string, then write a `load()` stub where you move your loading code. This method resets `loadedJoke `and also returns `Joke`, so you don't need the Joke property for this simple example, but you probably need it for more complex data.

Then create a `JokeServiceStore `object in the `JokeService `(in a class, not an extension):

```
private let store = JokeServiceStore()
```

Now move the `url `code from `JokeService `to `JokeServiceStore`:

```
private var url: URL {
  urlComponents.url!
}

private var urlComponents: URLComponents {
  var components = URLComponents()
  components.scheme = "https"
  components.host = "api.chucknorris.io"
  components.path = "/jokes/random"
  components.setQueryItems(with: ["category": "dev"])
  return components
}
```

Then move the loading code from `fetchJoke()` to `load()` leaving only two `isFetching `lines in `fetchJoke()`:

```
// move this code from fetchJoke() to load()
let (data, response) = try await URLSession.shared.data(from: url)
guard
  let httpResponse = response as? HTTPURLResponse,
  httpResponse.statusCode == 200
else {
  throw DownloadError.statusNotOk
}
guard let decodedResponse = try? JSONDecoder().decode(Joke.self, from: data)
else { throw DownloadError.decoderError }
joke = decodedResponse.value
```

`JokeServiceStore `has a `Joke `property, not a `String `property, so replace the last line with the following code:

```
loadedJoke = decodedResponse
return loadedJoke
```

Instead of retrieving just the value from the `decodedResponse`, you set the `Joke `property and also return that `Joke `instance.

Now call `load()` in `fetchJoke()`:

```
let loadedJoke = try await store.load()
joke = loadedJoke.value
```

Build and run.

A joke appears, but you have purple warnings:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22kpb80lsn8hl80k09jf.png)

Posting changes from background threads is not allowed.

Add a breakpoint inside `load()` and in `fetchJoke()` in `isFetching = true`, `let loadedJoke = ...` and `joke = loadedJoke.value;`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe1surv4diwryhmynvxt.png)

Setting breakpoints.

Click the button again, then watch the threads by clicking **Continue program execution** after each dot:
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/us2hyes1kr7a2ipwrrkc.png)

`fetchJoke()` runs on the main thread but goes to a background thread.

The first two lines of `fetchJoke()` are executed on the main thread because view is calling it. The `load()` is then run on a background thread, as it should be. But when execution returns to `fetchJoke()` it is still on a background thread. You need to do something to make it run on the main thread.

## @MainActor
The code that sets the `Published `value must run on the `MainActor `thread. When `fetchJoke()` did all the work and you called it from a Button in an unstructured task, `fetchJoke()` inherited `MainActor `from `Button `and all of its code was running on the `MainActor `thread.

Now `fetchJoke()` calls `load()` which runs on a background thread. `fetchJoke() `still runs on the main thread, but when `load()` completes, `fetchJoke()` continues to run on the background thread.

`fetchJoke()` should not rely on `MainActor `being inherited from `Button`. You can mark a class or function with the `@MainActor` attribute to indicate that it should run on the `MainActor `thread.

**The note**
If you mark a class as `@MainActor`, any calls from outside `MainActor `must be `await`, even if it's a method call that exits immediately. A method that does not refer to any mutable state may drop the `MainActor `with the `nonisolated `keyword.

Add this line above `func fetchJoke() throws {`

```
@MainActor
```

Compile and run again and hit breakpoints:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1d1ilocw6l24ruy5pgm.png)

`fetchJoke() `runs on the main thread after `load()` completes.

The first three points are the same as before, but now `fetchJoke() `runs on the main thread after `load()` completes.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/78bagrgujys566ux19ox.png)

When `fetchJoke()` calls `load()` , it pauses, freeing up the main thread for UI tasks. When the download completes, `fetchJoke()` runs again on the main thread, where `Published `is allowed to be set.

Your work is done here! Try converting your own SwiftUI projects: take it slow, make small changes, and try to keep the app buildable after each change.

## Optional: Asynchronous View Modifiers
SwiftUI now has (at least) two view modifiers that expect their action to call an async function.

Create a new SwiftUI View file named `RefreshableView.swift` and replace the contents of RefreshableView with the following:

```
@StateObject var jokeService = JokeService()

var body: some View {
  List {
    Text("Chuck Norris Joke")
      .font(.largeTitle)
      .listRowSeparator(.hidden)
    Text(jokeService.joke)
      .multilineTextAlignment(.center)
      .lineLimit(nil)
      .lineSpacing(5.0)
      .padding()
      .font(.title)
  }
  .task {
    try? await jokeService.fetchJoke()
  }
  .refreshable {
    try? await jokeService.fetchJoke()
  }
}
```

1. This view is a `List `because `refreshable(action:)` only works on scrollable views.

2. The task modifier performs its action when the view appears. The type of its action parameter is `action - @escaping() async -> Void`. It creates a task to run the action so you don't need it.

3. The type of the `action `parameter of the `refreshable `modifier is the same. It must be asynchronous. When applied to a scrollable view, the user can drag down to refresh its content, and it displays a refresh indicator until the asynchronous task completes.

Launch **Live Preview**. The joke comes up:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vq1rcgq3inzik5tuxclt.png)

The joke appears when the view is loaded.

> **The note**
> It's actually an O(N) complexity algorithm.

Pull down to get another joke. You may need to pull down quite far.

If you want to run this version in the simulator or device, open `WaitForItApp.swift` and change `ContentView()` to `RefreshableView()` in the `WindowGroup `closure.

In this tutorial, you converted a simple SwiftUI app from the old GCD threading implementation to the new Swift threading implementation using async/await, unstructured task, actor, and @MainActor.