Revealed on: April 12, 2022
In my earlier put up you discovered about some totally different use circumstances the place you may need to decide on between an async sequence and Mix whereas additionally clearly seeing that async sequence are virtually all the time higher wanting within the examples I’ve used, it’s time to take a extra practical have a look at the way you may be utilizing every mechanism in your apps.
The main points on how the lifecycle of a Mix subscription or async for-loop ought to be dealt with will fluctuate based mostly on the way you’re utilizing them so I’ll be offering examples for 2 conditions:
- Managing your lifecycles in SwiftUI
- Managing your lifecycles nearly anyplace else
We’ll begin with SwiftUI because it’s by far the simplest state of affairs to cause about.
Managing your lifecycles in SwiftUI
Apple has added a bunch of very handy modifiers to SwiftUI that enable us to subscribe to publishers or launch an async activity with out worrying concerning the lifecycle of every an excessive amount of. For the sake of getting an instance, let’s assume that we’ve an object that exists in our view that appears a bit like this:
class ExampleViewModel {
func notificationCenterPublisher() -> AnyPublisher<UIDeviceOrientation, By no means> {
NotificationCenter.default.writer(for: UIDevice.orientationDidChangeNotification)
.map { _ in UIDevice.present.orientation }
.eraseToAnyPublisher()
}
func notificationCenterSequence() async -> AsyncMapSequence<NotificationCenter.Notifications, UIDeviceOrientation> {
await NotificationCenter.default.notifications(named: UIDevice.orientationDidChangeNotification)
.map { _ in await UIDevice.present.orientation }
}
}
Within the SwiftUI view we’ll name every of those two features to subscribe to the writer in addition to iterate over the async sequence. Right here’s what our SwiftUI view seems to be like:
struct ExampleView: View {
@State var isPortraitFromPublisher = false
@State var isPortraitFromSequence = false
let viewModel = ExampleViewModel()
var physique: some View {
VStack {
Textual content("Portrait from writer: (isPortraitFromPublisher ? "sure" : "no")")
Textual content("Portrait from sequence: (isPortraitFromSequence ? "sure" : "no")")
}
.activity {
let sequence = await viewModel.notificationCenterSequence()
for await orientation in sequence {
isPortraitFromSequence = orientation == .portrait
}
}
.onReceive(viewModel.notificationCenterPublisher()) { orientation in
isPortraitFromPublisher = orientation == .portrait
}
}
}
On this instance I’d argue that the writer strategy is simpler to know and use than the async sequence one. Constructing the writer is nearly the identical as it’s for the async sequence with the most important distinction being the return sort of our writer vs. our sequence: AnyPublisher<UIDeviceOrientation, By no means>
vs. AsyncMapSequence<NotificationCenter.Notifications, UIDeviceOrientation>
. The async sequence truly leaks its implementation particulars as a result of we’ve to return an AsyncMapSequence
as a substitute of one thing like an AnyAsyncSequence<UIDeviceOrientation>
which might enable us to cover the inner particulars of our async sequence.
Right now it doesn’t appear to be the Swift crew sees any profit in including one thing like eraseToAnyAsyncSequence()
to the language so we’re anticipated to offer absolutely certified return sorts in conditions like ours.
Utilizing the sequence can be just a little bit more durable in SwiftUI than it’s to make use of the writer. SwiftUI’s onReceive
will deal with subscribing to our writer and it’ll present the writer’s output to our onReceive
closure. For the async sequence we are able to use activity
to create a brand new async context, get hold of the sequence, and iterate over it. Not a giant deal however positively just a little extra complicated.
When this view goes out of scope, each the Activity
created by activity
in addition to the subscription created by onReceive
shall be cancelled. Which means that we don’t want to fret concerning the lifecycle of our for-loop and subscription.
If you wish to iterate over a number of sequences, you may be tempted to jot down the next:
.activity {
let sequence = await viewModel.notificationCenterSequence()
for await orientation in sequence {
isPortraitFromSequence = orientation == .portrait
}
let secondSequence = await viewModel.anotherSequence()
for await output in secondSequence {
// deal with ouput
}
}
Sadly, this setup wouldn’t have the specified final result. The primary for-loop might want to end earlier than the second sequence is even created. This for-loop behaves similar to a daily for-loop the place the loop has to complete earlier than shifting on to the subsequent traces in your code. The truth that values are produced asynchronously doesn’t change this. To iterate over a number of async sequences in parallel, you want a number of duties:
.activity {
let sequence = await viewModel.notificationCenterSequence()
for await orientation in sequence {
isPortraitFromSequence = orientation == .portrait
}
}
.activity {
let secondSequence = await viewModel.anotherSequence()
for await output in secondSequence {
// deal with ouput
}
}
In SwiftUI, that is al comparatively easy to make use of, and it’s comparatively exhausting to make errors. However what occurs if we examine publishers and async sequences lifecycles exterior of SwiftUI? That’s what you’ll discover out subsequent.
Managing your lifecycles exterior of SwiftUI
While you’re subscribing to publishers or iterating over async sequences exterior of SwiftUI, issues change just a little. You out of the blue have to handle the lifecycles of the whole lot you do far more rigorously, or extra particularly for Mix you have to be sure you retain your cancellables to keep away from having your subscriptions being torn down instantly. For async sequences you’ll need to be sure you don’t have the duties that wrap your for-loops linger for longer than they need to.
Let’s have a look at an instance. I’m nonetheless utilizing SwiftUI, however all of the iterating and subscribing will occur in a view mannequin as a substitute of my view:
struct ContentView: View {
@State var showExampleView = false
var physique: some View {
Button("Present instance") {
showExampleView = true
}.sheet(isPresented: $showExampleView) {
ExampleView(viewModel: ExampleViewModel())
}
}
}
struct ExampleView: View {
@ObservedObject var viewModel: ExampleViewModel
@Atmosphere(.dismiss) var dismiss
var physique: some View {
VStack(spacing: 16) {
VStack {
Textual content("Portrait from writer: (viewModel.isPortraitFromPublisher ? "sure" : "no")")
Textual content("Portrait from sequence: (viewModel.isPortraitFromSequence ? "sure" : "no")")
}
Button("Dismiss") {
dismiss()
}
}.onAppear {
viewModel.setup()
}
}
}
This setup permits me to current an ExampleView
after which dismiss it once more. When the ExampleView
is offered I need to be subscribed to my notification heart writer and iterate over the notification heart async sequence. Nonetheless, when the view is dismissed the ExampleView
and ExampleViewModel
ought to each be deallocated and I would like my subscription and the duty that wraps my for-loop to be cancelled.
Right here’s what my non-optimized ExampleViewModel
seems to be like:
@MainActor
class ExampleViewModel: ObservableObject {
@Revealed var isPortraitFromPublisher = false
@Revealed var isPortraitFromSequence = false
personal var cancellables = Set<AnyCancellable>()
deinit {
print("deinit!")
}
func setup() {
notificationCenterPublisher()
.map { $0 == .portrait }
.assign(to: &$isPortraitFromPublisher)
Activity { [weak self] in
guard let sequence = await self?.notificationCenterSequence() else {
return
}
for await orientation in sequence {
self?.isPortraitFromSequence = orientation == .portrait
}
}
}
func notificationCenterPublisher() -> AnyPublisher<UIDeviceOrientation, By no means> {
NotificationCenter.default.writer(for: UIDevice.orientationDidChangeNotification)
.map { _ in UIDevice.present.orientation }
.eraseToAnyPublisher()
}
func notificationCenterSequence() -> AsyncMapSequence<NotificationCenter.Notifications, UIDeviceOrientation> {
NotificationCenter.default.notifications(named: UIDevice.orientationDidChangeNotification)
.map { _ in UIDevice.present.orientation }
}
}
Should you’d put the views in a challenge together with this view mannequin, the whole lot will look good on first sight. The view updates as anticipated and the ExampleViewModel
’s deinit
known as every time we dismiss the ExampleView
. Let’s make some modifications to setup()
to double test that each our Mix subscription and our Activity
are cancelled and now not receiving values:
func setup() {
notificationCenterPublisher()
.map { $0 == .portrait }
.handleEvents(receiveOutput: { _ in print("subscription obtained worth") })
.assign(to: &$isPortraitFromPublisher)
Activity { [weak self] in
guard let sequence = self?.notificationCenterSequence() else {
return
}
for await orientation in sequence {
print("sequence obtained worth")
self?.isPortraitFromSequence = orientation == .portrait
}
}.retailer(in: &cancellables)
}
Should you run the app now you’ll discover that you simply’ll see the next output if you rotate your gadget or simulator after dismissing the ExampleView
:
// current ExampleView and rotate
subscription obtained worth
sequence obtained worth
// rotate once more
subscription obtained worth
sequence obtained worth
// dismiss
deinit!
// rotate once more
sequence obtained worth
You’ll be able to see that the ExampleViewModel
is deallocated and that the subscription now not receives values after that. Sadly, our Activity
continues to be energetic and it’s nonetheless iterating over our async sequence. Should you current the ExampleView
once more, you’ll discover that you simply now have a number of energetic iterators. This can be a drawback as a result of we need to cancel our Activity
every time the article that comprises it’s deallocated, mainly what Mix does with its AnyCancellable
.
Fortunately, we are able to add a easy extension on Activity
to piggy-back on the mechanism that makes AnyCancellable
work:
extension Activity {
func retailer(in cancellables: inout Set<AnyCancellable>) {
asCancellable().retailer(in: &cancellables)
}
func asCancellable() -> AnyCancellable {
.init { self.cancel() }
}
}
Mix’s AnyCancellable
is created with a closure that’s run every time the AnyCancellable
itself shall be deallocated. On this closure, the duty can cancel itself which may even cancel the duty that’s producing values for our for-loop. This could finish the iteration so long as the duty that produces values respects Swift Concurrency’s activity cancellation guidelines.
Now you can use this extension as follows:
Activity { [weak self] in
guard let sequence = self?.notificationCenterSequence() else {
return
}
for await orientation in sequence {
print("sequence obtained worth")
self?.isPortraitFromSequence = orientation == .portrait
}
}.retailer(in: &cancellables)
Should you run the app once more, you’ll discover that you simply’re now not left with extraneous for-loops being energetic which is nice.
Identical to earlier than, iterating over a second async sequence requires you to create a second activity to carry the second iteration.
In case the duty that’s producing your async values doesn’t respect activity cancellation, you could possibly replace your for-loop as follows:
for await orientation in sequence {
print("sequence obtained worth")
self?.isPortraitFromSequence = orientation == .portrait
if Activity.isCancelled { break }
}
This merely checks whether or not the duty we’re at present in is cancelled, and whether it is we escape of the loop. You shouldn’t want this so long as the worth producing activity was applied accurately so I wouldn’t suggest including this to each async for-loop you write.
Abstract
On this put up you discovered rather a lot about how the lifecycle of a Mix subscription compares to that of a activity that iterates over an async sequence. You noticed that utilizing both in a SwiftUI view modifier was fairly easy, and SwiftUI makes managing lifecycles simple; you don’t want to fret about it.
Nonetheless, you additionally discovered that as quickly as we transfer our iterations and subscriptions exterior of SwiftUI issues get messier. You noticed that Mix has good built-in mechanisms to handle lifecycles via its AnyCancellable
and even its assign(to:)
operator. Duties sadly lack an analogous mechanism which implies that it’s very simple to finish up with extra iterators than you’re snug with. Fortunately, we are able to add an extension to Activity
to care for this by piggy-backing on Mix’s AnyCancellable
to cancel our Activity
objects as quickly s the article that owns the duty is deallocated.
All in all, Mix merely gives extra handy lifecycle administration out of the field once we’re utilizing it exterior of SwiftUI views. That doesn’t imply that Mix is robotically higher, nevertheless it does imply that async sequences aren’t fairly in a spot the place they’re as simple to make use of as Mix. With a easy extension we are able to enhance the ergonomics of iterating over an async sequence by rather a lot, however I hope that the Swift crew will tackle binding activity lifecycles to the lifecycle of one other object like Mix does in some unspecified time in the future sooner or later.