Common Continuation Patterns

Let's look at common continuation patterns you'll use in real iOS apps.
Follow along with the code: iOS-Practice on GitHub
UIAlertController Response
Wrap alert actions as async:
extension UIViewController {
func showConfirmation(title: String, message: String) async -> Bool {
await withCheckedContinuation { continuation in
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
continuation.resume(returning: false)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
continuation.resume(returning: true)
})
present(alert, animated: true)
}
}
}
// Usage
if await showConfirmation(title: "Delete?", message: "This cannot be undone.") {
deleteItem()
}
Photo Picker
Wrap PHPickerViewController:
func pickPhoto() async -> UIImage? {
await withCheckedContinuation { continuation in
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = PickerDelegate { results in
let image = results.first.flatMap { result -> UIImage? in
// Load image from result...
return loadedImage
}
continuation.resume(returning: image)
}
present(picker, animated: true)
}
}
One-Shot Location
Get current location once:
class LocationFetcher: NSObject, CLLocationManagerDelegate {
private var continuation: CheckedContinuation<CLLocation, Error>?
private let manager = CLLocationManager()
func getCurrentLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
continuation?.resume(returning: locations[0])
continuation = nil
}
func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}
Animation Completion
Wait for animation to finish:
func animate(view: UIView, to point: CGPoint) async {
await withCheckedContinuation { continuation in
UIView.animate(withDuration: 0.3, animations: {
view.center = point
}, completion: { _ in
continuation.resume()
})
}
}
// Sequential animations become linear
await animate(view: box, to: CGPoint(x: 100, y: 100))
await animate(view: box, to: CGPoint(x: 200, y: 100))
await animate(view: box, to: CGPoint(x: 200, y: 200))
Document Picker
func pickDocument() async -> URL? {
await withCheckedContinuation { continuation in
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf])
picker.delegate = DocumentDelegate { urls in
continuation.resume(returning: urls.first)
}
present(picker, animated: true)
}
}
The Delegate Wrapper Pattern
For delegate-based APIs, create a small wrapper class:
class DelegateHandler<T>: NSObject {
private var continuation: CheckedContinuation<T, Never>?
func setContinuation(_ continuation: CheckedContinuation<T, Never>) {
self.continuation = continuation
}
func complete(with value: T) {
continuation?.resume(returning: value)
continuation = nil
}
}
Common Mistakes
1. Not handling cancellation:
// If view is dismissed before callback, continuation never resumes
picker.delegate = Delegate { result in
continuation.resume(returning: result)
}
2. Multiple code paths:
// Both success and dismissal can trigger - need coordination
picker.onSelect = { item in continuation.resume(returning: item) }
picker.onCancel = { continuation.resume(returning: nil) } // Could conflict!
Interview Tip
These patterns show practical async/await usage. When discussing UI code, mention that picker/alert continuation wrappers dramatically simplify flow—no more nested completion handlers.