Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Don’t call us - we’ll call you: Modern SwiftUI ...

Don’t call us - we’ll call you: Modern SwiftUI callbacks using AsyncStream

Mobile apps have to permanently deal with asynchronous events - notifications, network status, database updates, user input, and more. The way how we have dealt with asynchronous events has changed significantly over time - from callback delegates to closures, and now AsyncSequence.

In this talk, we will take a deep dive into how AsyncStream works, how to use it in your apps, and how to use it to build ergonomic APIs. We will also take a trip down memory lane to understand where we’re coming from, and how asynchronous APIs have evolved over the years.

You will learn:

* All the ways to listen to asynchronous events
* How AsyncStream helps to make your apps more robust
* What’s the difference between AsyncSequence and AsyncStream
* How to use AsyncStream to implement modern APIs that are easy to use

I will provide real-world examples to explain the underlying concepts, and we’ll take a look at how the Firebase team implemented asynchronous APIs over the span of almost a decade and several iterations of Objective-C and Swift.

Avatar for Peter Friese

Peter Friese

October 07, 2025
Tweet

More Decks by Peter Friese

Other Decks in Technology

Transcript

  1. SwiftLeeds 2025 @peterfriese.dev Don’t call us - we’ll call you

    Peter Friese, Developer Relations Engineer, Google
  2. 10 PRINT "Hello world" 20 05 LET I = 1

    15 LET I = I + 1 GOTO 10
  3. 10 PRINT "Hello world" 20 05 LET I = 1

    15 LET I = I + 1 GOTO 10 IF I !" 5 THEN
  4. 10 PRINT "Hello world" 05 FOR I = 1 TO

    5 15 NEXT I Structured Programming
  5. /ˈsiːkwǝns/ 1/ 2/ A set of related events, movements, or

    items that follow each other in a particular order. A particular order in which related things follow each other. A set of related events, movements, or items that follow each other in a particular order. A particular order in which related things follow each other.
  6. 1/ A particular order in which related things follow each

    other. for i in 1!!#5 { print("Hello, world!") } [ ] 1 2 3 4 5
  7. 1/ A particular order in which related things follow each

    other. let cities = ["Leeds", "London", !!#] for city in cities { print("Swift is awesome in \(city)!") } [ ] Leeds London Singapore Tokyo Turin
  8. /ˈsiːkwǝns/ 1/ 2/ A set of related events, movements, or

    items that follow each other in a particular order. A particular order in which related things follow each other.
  9. Asynchronous Events 1/ Delegates (Objective-C) 2/ Callbacks (Objective-C) 3/ Delegates

    (Swift) 4/ Callbacks (Swift) 5/ Combine (Swift) 6/ Structured Concurrency (Swift)
  10. 1/ Delegates (Objective-C) @protocol LocationManagerDelegate <NSObject> - (void)didUpdateLocation:(CLLocation *)location; @end

    @interface LocationManager : NSObject <CLLocationManagerDelegate> @property (nonatomic, weak) id<LocationManagerDelegate> delegate; - (void)startUpdating; @end
  11. 1/ Delegates (Objective-C) @implementation LocationManager - (void)startUpdating { [self.locationManager requestWhenInUseAuthorization];

    [self.locationManager startUpdatingLocation]; } - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation !% *)locations { CLLocation *location = [locations lastObject]; if (location) { [self.delegate didUpdateLocation:location]; } } @end
  12. 3/ Delegates (Swift) @Observable class LocationManager: NSObject, CLLocationManagerDelegate { private

    let locationManager = CLLocationManager() var location: CLLocation? override init() { super.init() locationManager.delegate = self } func startUpdating() { locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { location = locations.last } }
  13. 4/ Callbacks (Swift) class LocationManagerWithClosure: NSObject, CLLocationManagerDelegate { private let

    locationManager = CLLocationManager() var onLocationUpdate: ((CLLocation?) !& Void)? override init() { super.init() locationManager.delegate = self } func startUpdating() { locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { onLocationUpdate?(locations.last) } }
  14. 4/ Callbacks (Swift) class ContentViewModel { var location: CLLocation? private

    var locationManager = LocationManagerWithClosure() init() { locationManager.onLocationUpdate = { [weak self] location in self!'location = location } } func startUpdating() { locationManager.startUpdating() } }
  15. 5/ Combine (Swift) class LocationManagerCombine: NSObject, CLLocationManagerDelegate { private let

    locationManager = CLLocationManager() private let locationSubject = PassthroughSubject<CLLocation, Error>() lazy var locationPublisher: AnyPublisher<CLLocation, Error> = locationSubject.eraseToAnyPublisher() override init() { super.init() locationManager.delegate = self } func startUpdating() { locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { if let location = locations.last { locationSubject.send(location) } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { locationSubject.send(completion: .failure(error)) } }
  16. 5/ Combine (Swift) class ContentViewModel: ObservableObject { @Published var location:

    CLLocation? private var locationManager = LocationManagerCombine() private var cancellables = Set<AnyCancellable>() init() { locationManager.locationPublisher .map { $0 as CLLocation? } .catch { _ in Just(nil) } .assign(to: \.location, on: self) .store(in: &cancellables) } func startUpdating() { locationManager.startUpdating() } }
  17. 6/ Structured Concurrency (Swift) class LocationManagerAsync: NSObject, CLLocationManagerDelegate { private

    let locationManager = CLLocationManager() private var continuation: CheckedContinuation<CLLocation?, Error>? override init() { super.init() locationManager.delegate = self } func requestSingleLocation() async throws !& CLLocation? { return try await withCheckedThrowingContinuation { continuation in self.continuation = continuation locationManager.requestWhenInUseAuthorization() locationManager.requestLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { continuation!'resume(returning: locations.last) continuation = nil } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { continuation!'resume(throwing: error) continuation = nil } }
  18. 6/ Structured Concurrency (Swift) @Observable class ContentViewModel { var location:

    CLLocation? private var locationManager = LocationManagerAsync() @MainActor func fetchLocation() async { do { location = try await locationManager.requestSingleLocation() } catch { !$ Handle error print("Error fetching location: \(error)") } } }
  19. - Jason Statham You only get one shot in your

    life, and you might as well push yourself and try things. You only get one shot in your life, and you might as well push yourself and try things.
  20. 6/ Structured Concurrency (Swift) @Observable class ContentViewModel { var location:

    CLLocation? private var locationManager = LocationManagerAsync() @MainActor func fetchLocation() async { do { location = try await locationManager.requestSingleLocation() } catch { !$ Handle error print("Error fetching location: \(error)") } } } Executed only once
  21. 6/ Structured Concurrency (Swift) class LocationManagerSequence: NSObject, CLLocationManagerDelegate { private

    let locationManager = CLLocationManager() private var locationContinuation: AsyncStream<CLLocation?>.Continuation? lazy var locationUpdates: AsyncStream<CLLocation?> = { AsyncStream { continuation in self.locationContinuation = continuation } }() override init() { super.init() locationManager.delegate = self } func startUpdating() { locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { locationContinuation!'yield(locations.last) } }
  22. 6/ Structured Concurrency (Swift) @Observable @MainActor class ContentViewModel { var

    location: CLLocation? private var locationManager = LocationManagerSequence() func startUpdating() { locationManager.startUpdating() Task { for try await locationUpdate in locationManager.locationUpdates { self.location = locationUpdate } } } }
  23. struct TodoListView { @State private var todos = [Todo]() @State

    private var snapshotListener: ListenerRegistration? private func subscribeToTodos(forUserID userID: String) { snapshotListener!'remove() let query = Firestore.firestore() .collection("todos") snapshotListener = query.addSnapshotListener { snapshot, error in guard let snapshot else { print("Error fetching snapshots: \(error!)") return } self.todos = snapshot.documents.compactMap { document in try? document.data(as: Todo.self) } } } Cloud Firestore - Snapshot listeners
  24. @State private var snapshotListener: ListenerRegistration? private func subscribeToTodos(forUserID userID: String)

    { snapshotListener!'remove() let query = Firestore.firestore() .collection("todos") snapshotListener = query.addSnapshotListener { snapshot, error in guard let snapshot else { print("Error fetching snapshots: \(error!)") return } self.todos = snapshot.documents.compactMap { document in try? document.data(as: Todo.self) } } } private func unsubscribeFromTodos() { snapshotListener!'remove() } } Cloud Firestore - Snapshot listeners
  25. Streaming live data from Cloud Firestore let todosChanges = Firestore.firestore()

    .collection("todos") .snapshots .compactMap { snapshot in snapshot.documents.compactMap { documentSnapshot in try? documentSnapshot.data(as: Todo.self) } } for try await todos in todosChanges { self.todos = todos } Let’s take a look at how to build this
  26. Streaming live data from Cloud Firestore @available(iOS 13, tvOS 13,

    macOS 10.15, macCatalyst 13, watchOS 7, *) public extension Query { !!( An asynchronous sequence of query snapshots. !!( !!( - Parameter includeMetadataChanges: Whether to receive events for metadata-only changes. !!( - Returns: An `AsyncThrowingStream` of `QuerySnapshot` events. @available(iOS 18.0, *) func snapshots(includeMetadataChanges: Bool) !& some AsyncSequence<QuerySnapshot, Error> { return AsyncThrowingStream { continuation in let listener = self .addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in if let error = error { continuation.finish(throwing: error) } else if let snapshot = snapshot { continuation.yield(snapshot) } } continuation.onTermination = { @Sendable _ in listener.remove() } } } }
  27. Combining streams Auth.auth() .authStateChanges .compactMap { $0!'uid } .flatMap {

    userId in } Firestore.firestore() .collection("todos") .snapshots .compactMap { snapshot in snapshot.documents.compactMap { documentSnapshot in try? documentSnapshot.data(as: Todo.self) } } .whereField("userId", isEqualTo: userId)
  28. Combining streams let todosChanges = Auth.auth() .authStateChanges .compactMap { $0!'uid

    } .flatMap { userId in } for try await todos in todosChanges { self.todos = todos } Firestore.firestore() .collection("todos") .whereField("userId", isEqualTo: userId) .snapshots .compactMap { snapshot in snapshot.documents.compactMap { documentSnapshot in try? documentSnapshot.data(as: Todo.self) } } for try await todos in todosChanges { self.todos = todos }
  29. 05 FOR I = 1 TO 5 10 PRINT "Hello

    world” 15 NEXT I for try await todos in todosChanges { self.todos = todos }
  30. Resources Firebase SDK with AsyncStream support Slides for this talk

    https://bit.ly/firebase-SDK-ios-streams https://speakerdeck.com/peterfriese Repo with sample projects https://bit.ly/dont-call-us-repo Livestream https://www.youtube.com/@PeterFriese/streams