Chapters

Hide chapters

Modern Concurrency in Swift

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Section I: Modern Concurrency in Swift

Section 1: 11 chapters
Show chapters Hide chapters

5. Intermediate async/await & CheckedContinuation
Written by Marin Todorov

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, you worked through creating custom asynchronous sequences. At this point, you should already feel right at home when it comes to using AsyncSequence and AsyncStream.

You saw that wrapping existing APIs, like Timer and NotificationCenter, is very powerful, letting you reuse your tried-and-tested code in your modern async/await codebase.

In this chapter, you’ll continue working in the same direction. You’ll look into more ways to reuse existing code to the fullest by leveraging Swift’s superpowered concurrency features.

Introducing continuations

Two patterns form the cornerstone of asynchronous programming on Apple platforms: callbacks and the delegate pattern. With completion callbacks, you pass in a closure that executes when the work completes. With the delegate pattern, you create a delegate object, then call certain methods on it when work progresses or completes:

result execution completion closure method(...) other code other code run completion data data data execution delegate delegate owner code call delegate method code call delegate method call delegate method

To encourage the new concurrency model’s adoption, Apple designed a minimal but powerful API that comes in handy when bridging existing code. It centers around the concept of a continuation.

A continuation is an object that tracks a program’s state at a given point. The Swift concurrency model assigns each asynchronous unit of work a continuation instead of creating an entire thread for it. This allows the concurrency model to scale your work more effectively based on the capabilities of the hardware. It creates only as many threads as there are available CPU cores, and it switches between continuations instead of between threads, making it more efficient.

You’re familiar with how an await call works: Your current code suspends execution and hands the thread and system resources over to the central handler, which decides what to do next.

When the awaited function completes, your original code resumes, as long as no higher priority tasks are pending. But how?

When the original code suspends, it creates a continuation that represents the entire captured state at the point of suspension. When it’s time to resume execution or throw, the concurrency system recreates the state from the continuation and the work… well, continues.

hello () async throws -> String { func Task.sleep ( ) try await nanoseconds: 1_000_000 return “Hello world” } try await hello() let result = print (result) suspend resume

This all happens behind the scenes when you use async functions. You can also create continuations yourself, which you can use to extend existing code that uses callbacks or delegates. These APIs can benefit from using await as well.

Manually creating continuations allows you to migrate your existing code gradually to the new concurrency model.

Creating continuations manually

There are two continuation API variants:

Wrapping the delegate pattern

In this chapter, you’ll continue working on the Blabber project, starting where you left off at the end of the last chapter. If you’ve worked through the challenges, just keep up the great work. Otherwise, you can start with this chapter’s starter project, which includes the solved challenge.

ejelugeit SBRamamiewZezazey xipa pilg dojegika dirwix koze loyb bawagona duxzuk dorq zesiqoje xamqon todipouc eltego selocoiy ucpara jidumuep erbopa

Managing the authorizations

You’ll get started by creating a location manager and verifying that the user has authorized the app to use the device location data. At this point, users who are running the app for the first time will see the standard system dialogue that asks them to grant authorization:

let location: CLLocation = try await 
withCheckedThrowingContinuation { [weak self] continuation in

}

SWIFT TASK CONTINUATION MISUSE: shareLocation() leaked its continuation!

Handling the location errors

Open Utility/ChatLocationDelegate.swift, where you’ll find the placeholder type ChatLocationDelegate. Notice that all the CLLocationManagerDelegate requirements are optional, so the file compiles without any of CLLocationManagerDelegate’s methods.

typealias LocationContinuation = CheckedContinuation<CLLocation, Error>
private var continuation: LocationContinuation?
init(manager: CLLocationManager, continuation: LocationContinuation) {
  self.continuation = continuation
  super.init()
  manager.delegate = self
  manager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
  switch manager.authorizationStatus {
  case .notDetermined:
    break
  case .authorizedAlways, .authorizedWhenInUse:
    manager.startUpdatingLocation()
  default:
    continuation?.resume(
      throwing: "The app isn't authorized to use location data"
    )
    continuation = nil
  }
}
func locationManager(
  _ manager: CLLocationManager,
  didUpdateLocations locations: [CLLocation]
) {
  guard let location = locations.first else { return }
  continuation?.resume(returning: location)
  continuation = nil
}
zufyacauloow = teg dudunouxx: vikopuon = qiziraukb. [ ] dagUpcuziDuzikeegy ZYLibazoaq ?. lemuxeuz) hoevz xux agnu salang ( : foffexaesuag rikubo cizobwawt } foyicaaf: = tec gby uviop ZVKajodiuc lednZpuhzezFjdaqurfGikgamainion { tajgeroekoon , poqr govufoahSuxiyif ( _ giyujej: JHYowepeulTefalup pumgx toviqi { { } ])

func locationManager(
  _ manager: CLLocationManager, 
  didFailWithError error: Error
) {  
  continuation?.resume(throwing: error)
  continuation = nil
}
tumodaoj: = hak ldl ayoab GHWivaveug xutdXzamqorNxsekorpMincewoamioy { xixpinueheov xuzlipiewuoz = gax utkem: ) { xotGaabSodjOvjav Umqiq , gafq rozuwoipHufocuz lavaciv: WXMapusoegRocihey ?. ( : azxiv) rapgameilooj zuguli knlaneqv } _ ( nyzub

efixofes cijo gijgivly yujuteb efakurot suwu ZxuzPozucuamButofefu
pex haud yerw obgog VdacNehuqaoyCaboveya
qon impaqa fopolaiwp BZFajuqaahVixevus zusbokeuxain.tumipo(...)

Using your delegate

Inside the closure of withCheckedThrowingContinuation(_:), insert the following:

self?.delegate = ChatLocationDelegate(manager: manager, continuation: continuation)
if manager.authorizationStatus == .authorizedWhenInUse {
  manager.startUpdatingLocation()
}
print(location.description)
manager.stopUpdatingLocation()
delegate = nil

<+19.01761470,+72.85616440> +/- 5.00m (speed -1.00 mps / course -1.00) ...

Wrapping callback APIs with continuation

In the system frameworks that Apple introduced after iOS 4, most asynchronous APIs are callback-based.

AuthorizationCenter.shared
  .requestAuthorization { result in
  
  }

Creating the closure

Open BlabberModel.swift and scroll back to the method called shareLocation(), where you added your delegate wrapping code.

let address: String = try await 
withCheckedThrowingContinuation { continuation in
  
}
AddressEncoder.addressFor(location: location) { address, error in

}
switch (address, error) {
case (nil, let error?):
  continuation.resume(throwing: error)
case (let address?, nil):
  continuation.resume(returning: address)
}
case (nil, nil):
  continuation.resume(throwing: "Address encoding failed")
case let (address?, error?):
  continuation.resume(returning: address)
  print(error)
try await say("📍 \(address)")

Challenges

Challenge: Build a command-line version of Blabber

This is an optional challenge that you can try on your own to exercise some of the concepts of the last few chapters.

Key points

  • You bridge older asynchronous design patterns to async/await by using CheckedContinuation or its unsafe counterpart, UnsafeCheckedContinuation.
  • For each of your code paths, you need to call one of the continuation’s resume(...) methods exactly once to either return a value or throw an error.
  • You get a continuation by calling either withCheckedContinuation(_:) or withCheckedThrowingContinuation(_:).
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now