Printed on: April 12, 2022
Swift 5.5 introduces async/await and an entire new concurrency mannequin that features a new protocol: AsyncSequence
. This protocol permits builders to asynchronously iterate over values coming from a sequence by awaiting them. Which means the sequence can generate or acquire its values asynchronously over time, and supply these values to a for-loop as they turn out to be accessible.
If this sounds acquainted, that’s as a result of a Mix writer does roughly the identical factor. A writer will acquire or generate its values (asynchronously) over time, and it’ll ship these values to subscribers at any time when they’re accessible.
Whereas the premise of what we will do with each AsyncSequence
and Writer
sounds comparable, I wish to discover a few of the variations between the 2 mechanisms in a collection of two posts. I’ll focus this comparability on the next matters:
- Use circumstances
- Lifecycle of a subscription / async for-loop
The put up you’re studying now will give attention to evaluating use circumstances. If you wish to study extra about lifecycle administration, check out this put up.
Please notice that components of this comparability will likely be extremely opinionated or be based mostly on my experiences. I’m attempting to guarantee that this comparability is honest, trustworthy, and proper however in fact my experiences and preferences will affect a part of the comparability. Additionally notice that I’m not going to invest on the futures of both Swift Concurrency nor Mix. I’m evaluating AsyncSequence
to Writer
utilizing Xcode 13.3, and with the Swift Async Algorithms package deal added to my challenge.
Let’s dive in, and try some present use circumstances the place publishers and async sequences can actually shine.
Operations that produce a single output
Our first comparability takes a more in-depth have a look at operations with a single output. Whereas this can be a acquainted instance for many of us, it isn’t the perfect comparability as a result of async sequences aren’t made for performing work that produces a single consequence. That’s to not say an async sequence can’t ship just one consequence, it completely can.
Nonetheless, you usually wouldn’t leverage an async sequence to make a community name; you’d await
the results of an information process as an alternative.
Alternatively, Mix doesn’t differentiate between duties that produce a single output and duties that produce a sequence of outputs. Which means publishers are used for operations that may emit many values in addition to for values that produce a single worth.
Mix’s method to publishers might be thought of an enormous good thing about utilizing them since you solely have one mechanism to study and perceive; a writer. It can be thought of a draw back since you by no means know whether or not an AnyPublisher<(Knowledge, URLResponse), Error>
will emit a single worth, or many values. Alternatively, let consequence: (Knowledge, URLResponse) = strive await getData()
will all the time clearly produce a single consequence as a result of we don’t use an async sequence to acquire a single consequence; we await
the results of a process as an alternative.
Despite the fact that this comparability technically compares Mix to async/await relatively than async sequences, let’s check out an instance of performing a community name with Mix vs. performing one with async/await to see which one appears to be like extra handy.
Mix:
var cancellables = Set<AnyCancellable>()
func getData() {
let url = URL(string: "https://donnywals.com")!
URLSession.shared.dataTaskPublisher(for: url)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
// deal with error
}
}, receiveValue: { (consequence: (Knowledge, URLResponse)) in
// use consequence
})
.retailer(in: &cancellables)
}
Async/Await:
func getData() async {
let url = URL(string: "https://donnywals.com")!
do {
let consequence: (Knowledge, URLResponse) = strive await URLSession.shared.information(from: url)
// use consequence
} catch {
// deal with error
}
}
For my part it’s fairly clear which know-how is extra handy for performing a process that produces a single consequence. Async/await is less complicated to learn, simpler to make use of, and requires far much less code.
With this considerably unfair comparability out of the way in which, let’s check out one other instance that permits us to extra instantly examine an async sequence to a writer.
Receiving outcomes from an operation that produces a number of values
Operations that produce a number of values are available many shapes. For instance, you is likely to be utilizing a TaskGroup
from Swift Concurrency to run a number of duties asynchronously, receiving the consequence for every process because it turns into accessible. That is an instance the place you’d use an async sequence to iterate over your TaskGroup
‘s outcomes. Sadly evaluating this case to Mix doesn’t make a whole lot of sense as a result of Mix doesn’t actually have an equal to TaskGroup
.
💡 Tip: to study extra about Swift Concurrency’s
TaskGroup
check out this put up.
One instance of an operation that may produce a number of values is observing notifications on NotificationCenter
. This can be a good instance as a result of not solely does NotificationCenter
produce a number of values, it’s going to achieve this asynchronously over an extended time frame. Let’s check out an instance the place we observe modifications to a person’s system orientation.
Mix:
var cancellables = Set<AnyCancellable>()
func notificationCenter() {
NotificationCenter.default.writer(
for: UIDevice.orientationDidChangeNotification
).sink(receiveValue: { notification in
// deal with notification
})
.retailer(in: &cancellables)
}
AsyncSequence:
func notificationCenter() async {
for await notification in await NotificationCenter.default.notifications(
named: UIDevice.orientationDidChangeNotification
) {
// deal with notification
}
}
On this case, there’s a bit much less of a distinction than after we used async/await to acquire the results of a community name. The primary distinction is in how we obtain values. In Mix, we use sink
to subscribe to a writer and we have to maintain on to the offered cancellable so the subscription is stored alive. With our async sequence, we use a particular for-loop the place we write for await <worth> in <sequence>
. Every time a brand new worth turns into accessible, our for-loop’s physique known as and we will deal with the notification.
When you have a look at this instance in isolation I don’t suppose there’s a really clear winner. Nonetheless, after we get to the benefit of use comparability you’ll discover that the comparability on this part doesn’t inform the complete story by way of the lifecycle and implications of utilizing an async sequence on this instance. The subsequent a part of this comparability will paint a greater image concerning this matter.
Let’s have a look at one other use case the place you would possibly end up questioning whether or not you need to attain for Mix or an async sequence; state commentary.
Observing state
When you’re utilizing SwiftUI in your codebase, you’re making in depth use of state commentary. The combo of @Printed
and ObservableObject
on information sources exterior to your view enable SwiftUI to find out when a view’s supply of reality will change so it may probably schedule a redraw of your view.
💡 Tip: If you wish to study extra about how and when SwiftUI determined to redraw views, check out this put up.
The @Printed
property wrapper is a particular type of property wrapper that makes use of Mix’s CurrentValueSubject
internally to emit values proper earlier than assigning these values because the wrapped property’s present worth. This implies you could subscribe to @Printed
utilizing Mix’s sink
to deal with new values as they turn out to be accessible.
Sadly, we don’t actually have an analogous mechanism accessible that solely makes use of Swift Concurrency. Nonetheless, for the sake of the comparability, we’ll make this instance work by leveraging the values
property on Writer
to transform our @Printed
writer into an async sequence.
Mix:
@Printed var myValue = 0
func stateObserving() {
$myValue.sink(receiveValue: { newValue in
}).retailer(in: &cancellables)
}
Async sequence:
@Printed var myValue = 0
func stateObserving() async {
for await newValue in $myValue.values {
// deal with new worth
}
}
Much like earlier than, the async sequence model appears to be like a little bit bit cleaner than the Mix model however as you’ll discover in the subsequent put up, this instance doesn’t fairly inform the complete story of utilizing an async sequence to watch state. The lifecycle of an async sequence can, in sure case complicate our instance rather a lot so I actually advocate that you just additionally try the lifecycle comparability to realize a a lot better understanding of an async sequence’s lifecycle.
It’s additionally vital to needless to say this instance makes use of Mix to facilitate the precise state commentary as a result of at the moment Swift Concurrency doesn’t present us with a built-in means to do that. Nonetheless, by changing the Mix writer to an async sequence we will get a reasonably good sense of what state commentary may appear to be if/when help for that is added to Swift.
Abstract
On this put up, I’ve lined three completely different use circumstances for each Mix and async sequences. It’s fairly clear that iterating over an async sequence appears to be like a lot cleaner than subscribing to a writer. There’s additionally little question that duties with a single output like community calls look a lot cleaner with async/await than they do with Mix.
Nonetheless, these examples aren’t fairly as balanced as I’d have preferred them to be. In all the Mix examples I took under consideration the lifecycle of the subscriptions I created as a result of in any other case the subscriptions wouldn’t work as a result of cancellable that’s returned by sink
being deallocated if it’s not retained in my set of cancellables.
The async sequence variations, nevertheless, work tremendous with none lifecycle administration however there’s a catch. Every of the capabilities I wrote was async
which implies that calling these capabilities have to be achieved with an await
, and the caller is suspended till the async sequence that we’re iterating over completes. Within the examples of NotificationCenter
and state commentary the sequences by no means finish so we’ll have to make some modifications to our code to make it work with out suspending the caller.
We’ll take a greater have a look at this within the subsequent put up.