WWDC Quick Look 💓 By SwiftGGTeam
Meet async/await in Swift

Meet async/await in Swift

Watch original video

async/await in Swift

Highlight

async/await makes writing asynchronous code as intuitive as synchronous code. A thumbnail acquisition function that originally required 20 lines of nested closures only requires 6 lines of linear code using async/await, and error handling is passedthrowsNatural communication, no longer need to judge at all levelserror != nil

Core Content

Asynchronous programming is an unavoidable topic in daily development, but writing methods based on completion handlers have a common problem: the code expands exponentially as the number of asynchronous steps increases. A network request, an image decoding, and a round of thumbnail generation - three steps of operations are nested. The indentation is bottomless, error handling is scattered in every corner, and a branch may be missed if you are not careful.

The introduction of async/await in Swift 5.5 fundamentally changes this situation. The core idea is simple: useasyncMark asynchronous functions withawaitSuspend the current execution flow to wait for the result, and the compiler is responsible for managing thread switching and state saving. The code reads like synchronous logic but executes non-blockingly.

Detailed Content

From Completion Handler to async/await

Take a function that gets thumbnails as an example. Traditional completion handler writing (03:43):

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()
}

This code has 20 lines, error handling is scattered in three places, and each branch must be called manuallycompletion. Use insteadResultAlthough types (08:00) make error handling more standardized, the amount of code has not been reduced:

func fetchThumbnail(for id: String, completion: @escaping (Result<UIImage, Error>) -> Void) {
    // ... same nested structure, except completion(.failure(error)) replaces completion(nil, error)
}

The async/await version (08:30) condenses the same logic into 6 lines:

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
}

Key changes:

  • asyncMark the function as asynchronous,throwsandasyncnatural combination
  • awaitSuspends the current execution and the thread is released to do other work
  • Error passedthrowsPropagating upwards eliminates the need to manually handle each branch
  • The execution order of the code is the reading order, from left to right, top to bottom

Asynchronous properties

UIKitprepareThumbnailThere are two versions: synchronous and asynchronous. can beUIImageDefine an asynchronous computed property (13:15):

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

Use directlyawait image.thumbnail, as natural as accessing normal properties.

Asynchronous sequence

When processing batch data, async/await andforLoops combined to form asynchronous sequences (14:17):

for await id in staticImageIDsURL.lines {
    let thumbnail = await fetchThumbnail(for: id)
    collage.add(thumbnail)
}
let result = await collage.draw()

staticImageIDsURL.linesis aAsyncSequence, each iterationawaitRead the next line. The entire loop is executed sequentially, but each timeawaitThe thread will be released.

Test asynchronous code

Testing completion handler style code requiresXCTestExpectation(21:22):

func testFetchThumbnails() throws {
    let expectation = XCTestExpectation(description: "mock thumbnails completion")
    self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
        XCTAssertNil(error)
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5.0)
}

The async/await version (21:56) can be called directly:

func testFetchThumbnails() async throws {
    XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
}

Calling asynchronous functions from synchronous code

SwiftUIonAppearIs the synchronization context and needs to be createdTaskto call an asynchronous function (22:30):

struct ThumbnailView: View {
    @ObservedObject var viewModel: ViewModel
    var post: Post
    @State private var image: UIImage?

    var body: some View {
        Image(uiImage: self.image ?? placeholder)
            .onAppear {
                Task {
                    self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
                }
            }
    }
}

Bridging existing API: CheckedContinuation

For third-party APIs that do not yet provide an async version, usewithCheckedThrowingContinuationPackaging (26:59):

// Existing completion handler API
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) { ... }

// Wrap as an async version
func persistentPosts() async throws -> [Post] {
    return try await withCheckedThrowingContinuation { continuation in
        self.getPersistentPosts { posts, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else {
                continuation.resume(returning: posts)
            }
        }
    }
}

For delegate callback mode, store the continuation as an instance variable (31:44):

class ViewController: UIViewController {
    private var activeContinuation: CheckedContinuation<[Post], Error>?

    func sharedPostsFromPeer() async throws -> [Post] {
        try await withCheckedThrowingContinuation { continuation in
            self.activeContinuation = continuation
            self.peerManager.syncSharedPosts()
        }
    }
}

extension ViewController: PeerSyncDelegate {
    func peerManager(_ manager: PeerManager, received posts: [Post]) {
        self.activeContinuation?.resume(returning: posts)
        self.activeContinuation = nil  // Prevent calling resume multiple times
    }
}

Asynchronous API in SDK

The iOS 15 SDK provides async alternatives for a number of existing APIs (25:56). For example, ClockKit’s complication data source:

extension ComplicationController: CLKComplicationDataSource {
    func currentTimelineEntry(for complication: CLKComplication) async -> CLKComplicationTimelineEntry? {
        let date = Date()
        let thumbnail = try? await self.viewModel.fetchThumbnail(for: post.id)
        guard let thumbnail = thumbnail else { return nil }
        return self.createTimelineEntry(for: thumbnail, date: date)
    }
}

Core Takeaways

  1. Prioritize migration of “assembly logic”: First change the top-level function that calls multiple asynchronous operations to async/await, and the underlying tool functions can gradually follow. This way the scope of changes is minimal and the benefits are maximal.

  2. usewithCheckedContinuationWrapping legacy APIs: For third-party libraries or internal APIs that have not yet been updated, use continuations to bridge them. Note that the callback is called immediatelyresume, don’t insert extra logic in the middle.

  3. Test code directly benefits: async/await makes asynchronous test code and synchronous test code written in the same way, no longer neededXCTestExpectationandwait(for:timeout:)

  4. Pay attention to the async alternative version of the SDK: Starting from iOS 15, URLSession, Core Data, ClockKit, etc. all provide async APIs. When migrating, give priority to using the native async version instead of packaging it yourself.

  5. TaskIt is a bridge from synchronization to asynchronous: inonAppearIBActionIn other synchronization contexts, useTask { }Create an asynchronous execution environment.Task.initInheriting the current actor context is the correct choice in most cases.

Comments

GitHub Issues · utterances