[ad_1]
Printed on: April 12, 2022
In my earlier submit you realized about some completely 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 at all times higher wanting within the examples I’ve used, it’s time to take a extra lifelike 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 must be dealt with will fluctuate primarily based 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 purpose 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 process with out worrying in regards to the lifecycle of every an excessive amount of. For the sake of getting an instance, let’s assume that we now have 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 appears to be like 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")")
}
.process {
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 key 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 now have to return an AsyncMapSequence as a substitute of one thing like an AnyAsyncSequence<UIDeviceOrientation> which might enable us to cover the interior particulars of our async sequence.
At the moment it doesn’t appear to be the Swift group sees any profit in including one thing like eraseToAnyAsyncSequence() to the language so we’re anticipated to offer absolutely certified return varieties in conditions like ours.
Utilizing the sequence can be slightly 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 process to create a brand new async context, get hold of the sequence, and iterate over it. Not a giant deal however undoubtedly slightly extra advanced.
When this view goes out of scope, each the Activity created by process in addition to the subscription created by onReceive can be cancelled. Because of this we don’t want to fret in regards to 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:
.process {
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 end 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 strains 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:
.process {
let sequence = await viewModel.notificationCenterSequence()
for await orientation in sequence {
isPortraitFromSequence = orientation == .portrait
}
}
.process {
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 laborious to make errors. However what occurs if we evaluate publishers and async sequences lifecycles exterior of SwiftUI? That’s what you’ll discover out subsequent.
Managing your lifecycles exterior of SwiftUI
If you’re subscribing to publishers or iterating over async sequences exterior of SwiftUI, issues change slightly. You immediately must handle the lifecycles of every little thing you do far more fastidiously, or extra particularly for Mix it’s good to ensure you retain your cancellables to keep away from having your subscriptions being torn down instantly. For async sequences you’ll need to ensure 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 middle writer and iterate over the notification middle 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 appears to be like like:
@MainActor
class ExampleViewModel: ObservableObject {
@Printed var isPortraitFromPublisher = false
@Printed var isPortraitFromSequence = false
non-public 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 }
}
}
For those who’d put the views in a mission together with this view mannequin, every little thing will look good on first sight. The view updates as anticipated and the ExampleViewModel’s deinit known as at any time when 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 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)
}
For those who run the app now you’ll discover that you just’ll see the next output while 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 not receives values after that. Sadly, our Activity continues to be energetic and it’s nonetheless iterating over our async sequence. For those who current the ExampleView once more, you’ll discover that you just now have a number of energetic iterators. This can be a downside as a result of we need to cancel our Activity at any time when the thing that accommodates it’s deallocated, principally 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 at any time when the AnyCancellable itself can be deallocated. On this closure, the duty can cancel itself which can 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 process 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)
For those who run the app once more, you’ll discover that you just’re 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 process to carry the second iteration.
In case the duty that’s producing your async values doesn’t respect process 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 presently in is cancelled, and whether it is we get away of the loop. You shouldn’t want this so long as the worth producing process was carried out accurately so I wouldn’t suggest including this to each async for-loop you write.
Abstract
On this submit you realized so much about how the lifecycle of a Mix subscription compares to that of a process that iterates over an async sequence. You noticed that utilizing both in a SwiftUI view modifier was fairly simple, and SwiftUI makes managing lifecycles straightforward; you don’t want to fret about it.
Nonetheless, you additionally realized 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 by way of its AnyCancellable and even its assign(to:) operator. Duties sadly lack the same mechanism which signifies that it’s very straightforward to finish up with extra iterators than you’re comfy with. Fortunately, we are able to add an extension to Activity to maintain this by piggy-backing on Mix’s AnyCancellable to cancel our Activity objects as quickly s the thing that owns the duty is deallocated.
All in all, Mix merely gives extra handy lifecycle administration out of the field after we’re utilizing it exterior of SwiftUI views. That doesn’t imply that Mix is routinely higher, but it surely does imply that async sequences aren’t fairly in a spot the place they’re as straightforward to make use of as Mix. With a easy extension we are able to enhance the ergonomics of iterating over an async sequence by so much, however I hope that the Swift group will tackle binding process lifecycles to the lifecycle of one other object like Mix does in some unspecified time in the future sooner or later.
[ad_2]
