Tuesday, August 9, 2022
HomeiOS DevelopmentEvaluating use instances for async sequences and publishers – Donny Wals

Evaluating use instances for async sequences and publishers – Donny Wals


Printed on: April 12, 2022

Swift 5.5 introduces async/await and a complete 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 develop into out there.

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 out there.

Whereas the premise of what we are able to do with each AsyncSequence and Writer sounds comparable, I want to discover among the variations between the 2 mechanisms in a collection of two posts. I’ll focus this comparability on the next matters:

  • Use instances
  • Lifecycle of a subscription / async for-loop

The publish you’re studying now will concentrate on evaluating use instances. If you wish to be taught extra about lifecycle administration, check out this publish.

Please observe that components of this comparability might be extremely opinionated or be primarily based on my experiences. I’m making an attempt to make it possible for this comparability is truthful, sincere, and proper however in fact 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 instances the place publishers and async sequences can really shine.

Operations that produce a single output

Our first comparability takes a more in-depth take a look at operations with a single output. Whereas it is 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 outcome. That’s to not say an async sequence can’t ship just one outcome, it completely can.

Nonetheless, you usually 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 will be thought-about an enormous advantage of utilizing them since you solely have one mechanism to be taught 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 outcome: (Information, URLResponse) = strive await getData() will at all times clearly produce a single outcome as a result of we don’t use an async sequence to acquire a single outcome; we await the results of a job as an alternative.

Despite the fact that this comparability technically compares Mix to async/await quite 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 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: { (outcome: (Information, URLResponse)) in
            // use outcome
        })
        .retailer(in: &cancellables)
}

Async/Await:

func getData() async {
    let url = URL(string: "https://donnywals.com")!
    do {
        let outcome: (Information, URLResponse) = strive await URLSession.shared.information(from: url)
        // use outcome
    } catch {
        // deal with error
    }
}

In my view it’s fairly clear which expertise is extra handy for performing a job that produces a single outcome. Async/await is simpler 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 permits us to extra immediately 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 perhaps utilizing a TaskGroup from Swift Concurrency to run a number of duties asynchronously, receiving the outcome for every job because it turns into out there. 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 numerous sense as a result of Mix doesn’t actually have an equal to TaskGroup.

💡 Tip: to be taught extra about Swift Concurrency’s TaskGroup check out this publish.

One instance of an operation that can produce a number of values is observing notifications on NotificationCenter. It is a good instance as a result of not solely does NotificationCenter produce a number of values, it’ll achieve this asynchronously over a protracted time period. Let’s check out an instance the place we observe modifications to a consumer’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 once we used async/await to acquire the results of a community name. The principle 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 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 out there, our for-loop’s physique is known as and we are able to deal with the notification.

In case you take a look at this instance in isolation I don’t assume there’s a really clear winner. Nonetheless, once we get to the benefit 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 matter.

Let’s take a look at one other use case the place you would possibly end up questioning whether or not it’s best to attain for Mix or an async sequence; state statement.

Observing state

In case you’re utilizing SwiftUI in your codebase, you’re making intensive use of state statement. The combination of @Printed and ObservableObject on information sources exterior to your view permit SwiftUI to find out when a view’s supply of fact will change so it may doubtlessly schedule a redraw of your view.

💡 Tip: If you wish to be taught extra about how and when SwiftUI determined to redraw views, check out this publish.

The @Printed property wrapper is a particular form 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 may subscribe to @Printed utilizing Mix’s sink to deal with new values as they develop into out there.

Sadly, we don’t actually have an analogous mechanism out there 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 a bit of bit cleaner than the Mix model however as you’ll discover in the following publish, 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 rather a lot so I actually suggest 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 vital to remember that this instance makes use of Mix to facilitate the precise state statement as a result of presently 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 are able to get a reasonably good sense of what state statement may seem like if/when help for that is added to Swift.

Abstract

On this publish, I’ve coated three completely different use instances for each Mix and async sequences. It’s fairly clear that iterating over an async sequence appears 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 might have appreciated them to be. In the entire 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, nonetheless, work fantastic with none lifecycle administration however there’s a catch. Every of the features I wrote was async which signifies that calling these features have to be finished 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 must make some modifications to our code to make it work with out suspending the caller.

We’ll take a greater take a look at this within the subsequent publish.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular