[WWDC21] Meet async/await in Swift

2 minute read

Functions

synchronous

  • blocks threads synchronous

asynchronous

  • use a completion handler asynchronous

Fetching a thumbnail

fetching fetchingcode

  • sync threads : thumbnailURLRequest, UIImage(data:)
  • async threads : dataTask(with:completion:), prepareThumnail(of:completionHandler:)

Async/Await

  • async : enable a function to suspend
  • await : marks where an async function may suspend execution
  • Once an awaited async call completes, execution resumes after the await
func fetchThumbnail(for id: String) async throws -> UIImage {
  let request = thumbnailURLRequest(for: id)
  let (data, response) = try await URLSession.shared.data(for: request)
  guard (reponse 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
}
  • threads : fetchThumbnail > thumbnailURLRequest(for:) > fetchThumbnail > data(for:) > … > fetchThumbnail > UIImage(data:) > fetchThumbnail > thumbnail > … > fetchThumbnail
  • awaitable(unblock thread ) : thumbnailURLRequest(for:), thumbnail
  • make it safer, shorter, better reflect your intent
extension UIImage {
  var thumbnail: UIImage? {
    get async {
      let size = CGSize(width: 40, height: 40)
      return await self.byPreparingThumbnail(ofSize: size)
    }
  }
}

Async sequence

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

Normal function call

normalfunction

An asynchronous function call

asynchronousfunction

Testing async code

  • async makes testing a snap

  • XCTestExpectation
    class MockVuewModelSpec: XCTestCase {
    func testFetchThumbnails() throws {
      let expectation = XCTestExpectation(description: "mock thumbnails completion")
      self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
        XCTAssertNil(error)
        expection.fulfill()
      }
      wait(for: [expection], timeout: 5.0)
    }
    }
    
  • async
    class MockVuewModelSpec: XCTestCase {
    func testFetchThumbnails() async throws {
    
      XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
    
    }
    }
    

Bridging from sync to async

  • Concurrency SwiftUI

  • sync
    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 {
          self.viewModel.fecthThumbnail(for: post.id) { result, _ in
            self.image = result
          }
        }
    }
    }
    
  • to async
    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 {
          async {
              self.image = try? await self.viewModel.fecthThumbnail(for: post.id)
          }
        }
    }
    }
    

Async alternatives and continuations

Continuations

  • Continuations must be resumes exactly once on every path

continuations

  • function old code
    func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {
    do {
      let req = Post.fetchRequest()
      req.sortDescriptor = [NSSortDescriptor(key: "date", ascending: true)]
      let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
        completion(result.finalResult ?? [], nil)
      }
      try self.managedObjectContext.execute(asyncRequest)
    } catch {
      completion([], error)
    }
    }
    
  • continuations code
    func persistentPosts() async throws -> [Post] {
    typealias PostContinuation = CheckedContinuation<[Post], Error>
    return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
      self.getPersistentPosts { posts, error in
        if let error = error {
          continuation.resume(throwing: error)
        } else {
          continuation.resume(returning: posts)
        }
      }
    }
    }
    

Continuations in Delegate

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(returing: posts)
    self.activeContinuation = nil // guard against multiple calls to resume
  }

  func peerManager(_ manager: peerManager, hadError error: Error) {
    self.activeContinuation?.resume(throwing: error)
    self.activeContinuation = nil
  }
}

Reference

Leave a comment