Advanced Concurrency

Common Continuation Patterns

August 5, 2026
5 min read
Featured image for blog post: 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.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch