You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since we’re assigning a property on our observable view model, we put this method on the @MainActor to ensure thread safety.
This single @MainActor attribute neatly replaces the .receive(on: RunLoop.main) operation. The for-await-in syntax serves as a suspension point between each iteration of the stream — the waiting itself is happening off the main thread.
Converting closure based handling to use AsyncStreams
We are passing in a closure to downloadAPI.startDownload() which takes in a percentage sourced from the startDownload() function and sends the percentage to the downloadSubject.
We can convert this to use an AsyncStream instead:
func performDownload()async->AsyncStream<Double>{AsyncStream{ continuation inTask{await downloadAPI.startDownload{ percentage in
continuation.yield(percentage)}
continuation.finish()}}}
AsyncStream objects are created by passing in a closure with a single continuation input parameter. The continuation is the bridge between the event source and the stream, call continuation.yield(...) to send values to the stream, or continuation.finish() to complete the stream.
The startDownload() function takes a closure that captures the continuation set when creating the AsyncStream. To use AsyncStream you need to have the event generator object access the continuation to send events to the stream.
Note that the closure is not passed as [weak self]. I wonder why not? The original version did use [weak self]
sequenceDiagram
participant View
participant ViewModel
participant AsyncStream
participant DownloadAPI
alt Stream Initialisation
View->>ViewModel: Task { events = await viewModel.performDownload() }
ViewModel->>AsyncStream: { continuation in ... }
AsyncStream->>DownloadAPI: Task { await downloadAPI.startDownload() }
end
alt Event Handling
DownloadAPI->>AsyncStream: continuation.yield/finished
AsyncStream->>ViewModel: update AsyncStream<Double>
ViewModel->>View: update events
end
Loading
Here, we are initializing the stream as well as starting the download immediately. This is represented by the two alt blocks Stream Initialisation and Event Handling. The AsyncViewModel and DownloadAPI class might look like this:
In this example, we are initializing the DownloadProgressDelegate object with a continuation when the AsyncStream is created. Later, when the DownloadProgressDelegate.urlSession(...) function is called, the continuation is used to send values to the AsyncStream by calls to the yield() function on the continuation.
sequenceDiagram
participant View
participant ViewModel
participant DownloadProgressDelegate
alt Stream Initialisation
View->>ViewModel: trackDownload()
ViewModel->>DownloadProgressDelegate: init(continuation)
DownloadProgressDelegate->>ViewModel: AsyncStream<Double>
ViewModel->>View: progress = AsyncStream<Double>
end
alt Start Download
View->>ViewModel: startDownload()
ViewModel->>DownloadProgressDelegate: urlSession(...)
DownloadProgressDelegate->>DownloadProgressDelegate: continuation.yield/finished
end
alt Event Handling
DownloadProgressDelegate->>ViewModel: update AsyncStream<Double>
ViewModel->>View: update progress
View->>View: for await event in progress
end
Loading
Here, we are initializing the stream, then starting the download from a request from the View, then handling the event in the View.
Here we specifying the scheduler to be RunLoop.main to ensure that updates to the view are executed on the main thread. A corresponding AsyncStream implementation might look like this:
We’re fetching data from UserAPI and sending the user data — or an error — to a CurrentValueSubject. This caches the latest value, making it easy to share the data across your app without performing lots of additional networking.
We can use an AsyncChannel instead of a CurrentValueSubject. AsyncChannel is a type heavily inspired by Combine subjects. It’s a special type of AsyncSequence which applies back pressure — this means it’ll buffer values. It waits for a value to be consumed downstream before calling the next() function on its iterator.
We begin by modifying the userSubject on our repository from a CurrentValueSubject to an AsyncThrowingChannel.
Jacobs Tech Tavern
AsyncAlgorithms
AsyncExtensions
Before / After
Repository Interface Changes
The repository interface using Combine:
A Swift concurrency interface:
We are
AsyncAlgorithmsto grant access to_throttleandcombineLatestfunctionsAsyncThrowingChannelfor access toUserobjects. The channel is used to provide single user eventsAsyncPublishertype to provide access to a Combine subects' values.AsyncStreamto access download progress events from theperformDownload()function.Setup Logic
we’ve also converted the setup code in the view model; from this classic Combine setup:
To use Swift concurrency:
Using
AsyncStreaminstead of CombineGet the values from a Combine subject
To combine two streams, use AsyncAlgorithms
From SwiftUI view:
This single
@MainActorattribute neatly replaces the.receive(on: RunLoop.main)operation. Thefor-await-insyntax serves as a suspension point between each iteration of the stream — the waiting itself is happening off the main thread.Converting closure based handling to use
AsyncStreamsIf we currently have
We are passing in a closure to
downloadAPI.startDownload()which takes in a percentage sourced from thestartDownload()function and sends the percentage to thedownloadSubject.We can convert this to use an
AsyncStreaminstead:AsyncStreamobjects are created by passing in a closure with a singlecontinuationinput parameter. Thecontinuationis the bridge between the event source and the stream, callcontinuation.yield(...)to send values to the stream, orcontinuation.finish()to complete the stream.The
startDownload()function takes a closure that captures thecontinuationset when creating theAsyncStream. To useAsyncStreamyou need to have the event generator object access thecontinuationto send events to the stream.sequenceDiagram participant View participant ViewModel participant AsyncStream participant DownloadAPI alt Stream Initialisation View->>ViewModel: Task { events = await viewModel.performDownload() } ViewModel->>AsyncStream: { continuation in ... } AsyncStream->>DownloadAPI: Task { await downloadAPI.startDownload() } end alt Event Handling DownloadAPI->>AsyncStream: continuation.yield/finished AsyncStream->>ViewModel: update AsyncStream<Double> ViewModel->>View: update events endHere, we are initializing the stream as well as starting the download immediately. This is represented by the two
altblocksStream InitialisationandEvent Handling. TheAsyncViewModelandDownloadAPIclass might look like this:and the corresponding SwiftUI view:
In this example, we are initializing the
DownloadProgressDelegateobject with acontinuationwhen theAsyncStreamis created. Later, when theDownloadProgressDelegate.urlSession(...)function is called, thecontinuationis used to send values to theAsyncStreamby calls to theyield()function on the continuation.sequenceDiagram participant View participant ViewModel participant DownloadProgressDelegate alt Stream Initialisation View->>ViewModel: trackDownload() ViewModel->>DownloadProgressDelegate: init(continuation) DownloadProgressDelegate->>ViewModel: AsyncStream<Double> ViewModel->>View: progress = AsyncStream<Double> end alt Start Download View->>ViewModel: startDownload() ViewModel->>DownloadProgressDelegate: urlSession(...) DownloadProgressDelegate->>DownloadProgressDelegate: continuation.yield/finished end alt Event Handling DownloadProgressDelegate->>ViewModel: update AsyncStream<Double> ViewModel->>View: update progress View->>View: for await event in progress endHere, we are initializing the stream, then starting the download from a request from the View, then handling the event in the View.
Updating the UI
The original Combine code to update the view:
Here we specifying the scheduler to be
RunLoop.mainto ensure that updates to the view are executed on the main thread. A correspondingAsyncStreamimplementation might look like this:@MainActorattribute indicates that this function is constrained to the main actor, which means that it will run on the main threadfor await inloop is used to access data from therepostream. This is a standard way of accessing the elements of anAsyncStreamobject.Using
AsyncChannelvs.CurrentValueSubjectWe can call an API and send values returned from the API to a
CurrentValueSubjectWe’re fetching data from UserAPI and sending the user data — or an error — to a CurrentValueSubject. This caches the latest value, making it easy to share the data across your app without performing lots of additional networking.
We can use an
AsyncChannelinstead of aCurrentValueSubject.AsyncChannelis a type heavily inspired by Combine subjects. It’s a special type ofAsyncSequencewhich applies back pressure — this means it’ll buffer values. It waits for a value to be consumed downstream before calling thenext()function on its iterator.We begin by modifying the userSubject on our repository from a CurrentValueSubject to an AsyncThrowingChannel.
We can do a straightforward conversion...
Updating the view
The original Combine version...
We can come up with a version that uses
AsyncChannelsequenceDiagram participant AsyncView participant AsyncStreamGenerator participant EventGeneratorSource AsyncView->>AsyncStreamGenerator: downloadEvents() async AsyncStreamGenerator->>EventGeneratorSource: init(AsyncStream<AsyncStreamStatus<Int>>.Continuation) AsyncStreamGenerator->>EventGeneratorSource: start() EventGeneratorSource->>AsyncStreamGenerator: continuation.yield, finish AsyncStreamGenerator->>AsyncView: AsyncStream<AsyncStreamStatus<Int>> AsyncView->>AsyncView: for await event in events