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 receive its values asynchronously over time, and supply these values to a for-loop as they change into obtainable.
If this sounds acquainted, that’s as a result of a Mix writer does roughly the identical factor. A writer will receive or generate its values (asynchronously) over time, and it’ll ship these values to subscribers at any time when they’re obtainable.
Whereas the idea of what we will do with each AsyncSequence
and Writer
sounds comparable, I want to discover among the variations between the 2 mechanisms in a sequence of two posts. I’ll focus this comparability on the next matters:
- Use circumstances
- Lifecycle of a subscription / async for-loop
The submit you’re studying now will deal with evaluating use circumstances. If you wish to study extra about lifecycle administration, check out this submit.
Please observe that components of this comparability can be extremely opinionated or be based mostly on my experiences. I’m making an attempt to make it possible for this comparability is honest, trustworthy, and proper however after all my experiences and preferences will affect a part of the comparability. Additionally observe that I’m not going to take a position 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 bundle added to my mission.
Let’s dive in, and try some present use circumstances the place publishers and async sequences can really 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 one of the best comparability as a result of async sequences aren’t made for performing work that produces a single end result. That’s to not say an async sequence can’t ship just one end result, it completely can.
Nonetheless, you sometimes wouldn’t leverage an async sequence to make a community name; you’d await
the results of a knowledge job as an alternative.
Then again, 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 strategy to publishers could be thought-about an enormous advantage of utilizing them since you solely have one mechanism to study and perceive; a writer. It may also be thought-about a draw back since you by no means know whether or not an AnyPublisher<(Information, URLResponse), Error>
will emit a single worth, or many values. Then again, let end result: (Information, URLResponse) = attempt await getData()
will at all times clearly produce a single end result as a result of we don’t use an async sequence to acquire a single end result; we await
the results of a job as an alternative.
Although this comparability technically compares Mix to async/await reasonably 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 seems 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: { (end result: (Information, URLResponse)) in
// use end result
})
.retailer(in: &cancellables)
}
Async/Await:
func getData() async {
let url = URL(string: "https://donnywals.com")!
do {
let end result: (Information, URLResponse) = attempt await URLSession.shared.knowledge(from: url)
// use end result
} catch {
// deal with error
}
}
For my part it’s fairly clear which know-how is extra handy for performing a job that produces a single end result. 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 best way, let’s check out one other instance that enables us to extra straight 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 in many shapes. For instance, you is likely to be utilizing a TaskGroup
from Swift Concurrency to run a number of duties asynchronously, receiving the end result for every job because it turns into obtainable. That is an instance the place you’ll use an async sequence to iterate over your TaskGroup
‘s outcomes. Sadly evaluating this case to Mix doesn’t make quite a 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 submit.
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’ll achieve this asynchronously over an extended time period. Let’s check out an instance the place we observe modifications to a consumer’s machine 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 once 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 supplied cancellable so the subscription is saved alive. With our async sequence, we use a particular for-loop the place we write for await <worth> in <sequence>
. Each time a brand new worth turns into obtainable, our for-loop’s physique is named 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, once we get to the convenience of use comparability you’ll discover that the comparability on this part doesn’t inform the total story when it comes to the lifecycle and implications of utilizing an async sequence on this instance. The subsequent a part of this comparability will paint a greater image relating to this subject.
Let’s have a look at one other use case the place you may end up questioning whether or not it is best to attain for Mix or an async sequence; state statement.
Observing state
When you’re utilizing SwiftUI in your codebase, you’re making intensive use of state statement. The combination of @Printed
and ObservableObject
on knowledge sources exterior to your view permit SwiftUI to find out when a view’s supply of reality will change so it may possibly doubtlessly 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 submit.
The @Printed
property wrapper is a particular sort 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 that you would be able to subscribe to @Printed
utilizing Mix’s sink
to deal with new values as they change into obtainable.
Sadly, we don’t actually have the same mechanism obtainable 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
}
}
Just like earlier than, the async sequence model seems a little bit bit cleaner than the Mix model however as you’ll discover in the subsequent submit, this instance doesn’t fairly inform the total story of utilizing an async sequence to look at state. The lifecycle of an async sequence can, in sure case complicate our instance quite a bit so I actually advocate that you simply additionally take a look at the lifecycle comparability to achieve a a lot better understanding of an async sequence’s lifecycle.
It’s additionally necessary to remember that this instance makes use of Mix to facilitate the precise state statement as a result of right now 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 fairly good sense of what state statement may appear like if/when help for that is added to Swift.
Abstract
On this submit, I’ve coated three totally different use circumstances for each Mix and async sequences. It’s fairly clear that iterating over an async sequence seems a lot cleaner than subscribing to a writer. There’s additionally little doubt 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 might have favored them to be. In all the Mix examples I took into consideration the lifecycle of the subscriptions I created as a result of in any other case the subscriptions wouldn’t work because of the cancellable that’s returned by sink
being deallocated if it’s not retained in my set of cancellables.
The async sequence variations, nevertheless, work nice with none lifecycle administration however there’s a catch. Every of the capabilities I wrote was async
which signifies 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 statement 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 submit.