SkillAgentSearch skills...

Asyncyawaitu

🏀 Tutorial - async/await in Swift

Install / Use

/learn @timdolenko/Asyncyawaitu
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Async/await was introduced in Swift 5.5 and if you still not sure how to use it - this tutorial should be more than enough to get you started!

We'll cover most of the real use cases with practical examples. Let's kick off!

<a href="https://youtube.com/playlist?list=PLDOdHHxI88cX5OOvCqubdYCORhJsNvx6E"><img width="215" alt="Screenshot 2022-08-15 at 15 55 41" src="https://user-images.githubusercontent.com/35912614/184648965-bbca0620-a3f2-4524-b249-6f94217f8c7a.png"></a>

Here's what we will cover:

  1. async functions
  2. async get properties
  3. async let
  4. Parallel execution using withTaskGroup and withThrowingTaskGroup
  5. Legacy APIs and withCheckedContinuation and withCheckedThrowingContinuation
  6. Create array of events or AsyncSequence with AsyncStream
  7. actor and nonisolated and why we need them

You can try each feature in the playground project you can grab here to follow the tutorial and run the code on your machine!

Foundations

Why?

Let's start with why? Why the hell do we need it? Have a look:

public func fetchThumbnail(for id: String, completion: @escaping (Result<Thumbnail, Error>) -> Void) {
    let request = thumbnailURLRequest(for: id)

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure(error))
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(.failure(FetchError.badID))
        } else {
            guard let image = UIImage(data: data!) else {
                completion(.failure(FetchError.badImage))
                return
            }

            image.prepareThumbnail(of: Thumbnail.size) { thumbnail in
                guard let thumbnail = thumbnail else {
                    completion(.failure(FetchError.badImage))
                    return
                }
                completion(.success(Thumbnail(id: id, image: thumbnail)))
            }
        }
    }
    task.resume()
}

Boy this looks ugly. And it's a typical async code we had in our apps, until now.

The first async function

Now let's make it pretty with async/await, and you'll see why we need it.

Let's keep this ugly monster here and create a new function.

public func fetchThumbnail(for id: String) async throws -> Thumbnail {
    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
    }

    guard let image = UIImage(data: data) else {
        throw FetchError.badImage
    }

    guard let thumbnail = await image.byPreparingThumbnail(ofSize: Thumbnail.size) else {
        throw FetchError.badImage
    }

    return Thumbnail(id: id, image: thumbnail)
}

First, we have to mark the function as async, it always goes before throws:

public func fetchThumbnail(for id: String) *async* throws -> Thumbnail
let (data, response) = try await URLSession.shared.data(for: request)

A lot of Apple APIs now have async alternatives for the functions we know.

Instead of passing the result through callback the function simply returns it and errors are thrown. That's something we do in our function too:

public func fetchThumbnail(for id: String) async *throws -> Thumbnail*

Now when we call a function like that, we have to await it on the callers side. It's done with await keyword which always goes after try. try is needed here to catch errors.

guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
guard let image = UIImage(data: data) else { throw FetchError.badImage }

Here we see a further benefit - no we can use throw to return from a function with error. You can't do it inside the callback!

image.byPreparingThumbnail is just another async function provided for us by Apple. Nice!

Compare the 2 now. Which one is easier to read?

Let's actually use the function now, let's go to the ViewModel and replace the old onAppear():

public func onAppear() async {
    do {
        let result = try await repository.fetchThumbnail(for: repository.randomId)

        DispatchQueue.main.async { [weak self] in
            self?.images = [result]
        }
    } catch {
        print(error)
    }
}

Now the function has to be marked async, and we handle errors with a do/catch block.

Let's go to the view now!

.onAppear { viewModel.onAppear() }
<img width="1079" alt="Screenshot 2022-08-15 at 16 53 47" src="https://user-images.githubusercontent.com/35912614/184659292-ded644d9-658e-48c9-b0d6-28dbbdae2511.png">

'async' call in a function that does not support concurrency screams at us!

How can we convert .onAppear {} to be async? - We can't! We use Task instead!

.onAppear {
    Task { await viewModel.onAppear() }
}

We dispatch a task (aka call an async function) this way from non-async context. It's a bridge between async and non-async worlds (and not only that).

Let's run the app now to see it work!

Not only functions (async get)

Btw. functions are not the only things that can be marked async. You can have async get properties now too!

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            await byPreparingThumbnail(ofSize: Thumbnail.size)
        }
    }
}

Let's use it in fetchThumbnail:

guard let thumbnail = await UIImage(data: data)?.thumbnail else { throw FetchError.badImage }

Parallel execution (async let)

Let's imagine we want to fetch several images, like this:

public func fetchThumbnails() async throws -> [Thumbnail] {
    let t1 = try await fetchThumbnail(for: "100")
    let t2 = try await fetchThumbnail(for: "101")

    return [t1, t2]
}

Looks simple right? But let's run the app, and before that, let's add some print statement to our fetchThumbnail(for id: String) async:

public func fetchThumbnail(for id: String) async throws -> Thumbnail {
    print("start \(id)")
    ...

    print("end \(id)")
    return ...
}

And don't forget to call the new function in the viewModel.onAppear().

Run the app! Here's what we see in the console:

start 100
end 100
start 101
end 101

We may also notice that loading is pretty slow, and that's because we get images one after the other, not in parallel.

How do we fix it? Actually, it's easy with async let. Add async before let in fetchThumbnails(), and remove await, like this:

async let t1 = try fetchThumbnail(for: "100")

And add try await on the return line:

public func fetchThumbnails() async throws -> [Thumbnail] {
    async let t1 = try fetchThumbnail(for: "100")
    async let t2 = try fetchThumbnail(for: "101")

    return try await [t1, t2]
}

Let's run! What do we see now?

start 100
start 101
end 100
end 101

We get them in parallel! Basically, when we type async let, we fire off the task, but we aren't waiting until it completes. We do it only when the variable is needed. In this case on the return line.

Task Groups

Now let's say we have an array of n elements, let's actually say we want to fetch all images for our ids array, that is located inside ThumnailRepositoryLive.

How would you do it? Here's how I would do it:

public func fetchThumbnails() async throws -> [Thumbnail] {
    var result = [Thumbnail]()

    for id in ids {
        let image = try await fetchThumbnail(for: id)
        result.append(image)
    }

    return result
}

Let's run and see that images are downloaded one after another once again!

That sucks! How do we fix it? async let won't help us here (you can try 😉).

Let's remove everything inside fetchThumbnails, and add:

public func fetchThumbnails() async throws -> [Thumbnail] {
    try await withThrowingTaskGroup(of: Thumbnail.self) { group in

        for id in ids {
            group.addTask { try await self.fetchThumbnail(for: id) }
        }

        var result = [Thumbnail]()

        for try await image in group {
            result.append(image)
        }

        return result
    }
}

That gives us a closure with group, that we can use to add tasks, like here:

group.addTask { try await self.fetchThumbnail(for: id) }

All we have to do is to await for them in a loop:

for try await image in group { ... }

Now let's run the app! What do we see now? The app is much faster! And have a look in the console - image downloading now happens in parallel!

How to convert callback to async/await?

In the example project we have 2 classes that represent delegate and callback APIs. Let's see how we provide async/await interface around them.

Here's our callback API:

public enum LuckySlotItem: String, CaseIterable {
    case star = "⭐️"
    case fire = "🔥"
    case rocket = "🚀"
    case finger = "👉"
    case frog = "🐸"
    case bug = "🐛"
}

public protocol LuckySlot {
    
    /// When you play, you will get result consisting of 3 `LuckySlotItems`
    func play(completion: @escaping (Result<[LuckySlotItem], Error>) -> ())
}

public class LuckySlotLive: LuckySlot {
    
    public func play(completion: @escaping (Result<[LuckySlotItem], Error>) -> ()) {
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.125) {
            completion(.success([
                LuckySlotItem.allCases.randomElement()!,
                LuckySlotItem.allCases.randomElement()!,
                LuckySlotItem.allCases.randomElement()!
            ]))
        }
    }
}

Here we just return 3 random emojis to some caller after a delay.

Let's create a wrapper for this class that would use async/await:

View on GitHub
GitHub Stars7
CategoryDevelopment
Updated7mo ago
Forks1

Languages

Swift

Security Score

67/100

Audited on Sep 3, 2025

No findings