Combine assign(to: on:) another publisher - ios

Here is a simple "Download" class to illustrate what I want to do.
class Download {
public var progress: CurrentValueSubject<Double, Never> = CurrentValueSubject<Double, Never>(0)
var subscriptions: Set<AnyCancellable> = []
func start(task: URLSessionTask) {
task.resume()
task.progress.publisher(for: \.fractionCompleted).sink { [weak self] (newProgress) in
self?.progress.send(newProgress)
}.store(in: &subscriptions)
}
}
I would like to be able to "re-publish" the progress property observer publisher to my current value subject. As you can see I currently subscribe using the .sink function and then just call the CurrentValueSubject publisher directly.
I would like to be able to use something like the .assign(to:, on:) operator like this.
task.progress.publisher(for: \.fractionCompleted).assign(to: \.progress, on: self)
However, that will not work nor will the .assign(to:) operator that seems to be reserved for "re-publishing" on a SwiftUI #Published property. Why is Combine not living up to it's name here?

Because CurrentValueSubject is a class, you can use it as the object argument of assign(to:on:). This way, there is no memory leak:
class Download {
public let progress: CurrentValueSubject<Double, Never> = .init(0)
var subscriptions: [AnyCancellable] = []
func start(task: URLSessionTask) {
task.resume()
task.progress
.publisher(for: \.fractionCompleted)
.assign(to: \.value, on: progress)
.store(in: &subscriptions)
}
}

You need to assign to the value of the subject, not the subject itself. It is worth noting though that assign(to: ..., on: self) leads to a memory leak 🤷🏻‍♀️.
func start(task: URLSessionTask) {
task.resume()
task.progress
.publisher(for: \.fractionCompleted)
.assign(to: \.progress.value, on: self)
.store(in: &subscriptions)
}

Related

How can I avoid this SwiftUI + Combine Timer Publisher reference cycle / memory leak?

I have the following SwiftUI view which contains a subview that fades away after five seconds. The fade is triggered by receiving the result of a Combine TimePublisher, but changing the value of showRedView in the sink publisher's sink block is causing a memory leak.
import Combine
import SwiftUI
struct ContentView: View {
#State var showRedView = true
#State var subscriptions: Set<AnyCancellable> = []
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onAppear {
fadeRedView()
}
}
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.sink { _ in
withAnimation {
showRedView = false
}
}
.store(in: &subscriptions)
}
}
I thought this was somehow managed behind the scenes with the AnyCancellable collection. I'm relatively new to SwiftUI and Combine, so sure I'm either messing something up here or not thinking about it correctly. What's the best way to avoid this leak?
Edit: Adding some pictures showing the leak.
Views should be thought of as describing the structure of the view, and how it reacts to data. They ought to be small, single-purpose, easy-to-init structures. They shouldn't hold instances with their own life-cycles (like keeping publisher subscriptions) - those belong to the view model.
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
.prefix(1)
.map { _ in }
.eraseToAnyPublisher()
}
}
And use .onReceive to react to published events in the View:
struct ContentView: View {
#State var showRedView = true
#ObservedObject vm = ViewModel()
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onReceive(self.vm.pub, perform: {
withAnimation {
self.showRedView = false
}
})
}
}
So, it seems that with the above arrangement, the TimerPublisher with prefix publisher chain is causing the leak. It's also not the right publisher to use for your use case.
The following achieves the same result, without the leak:
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Just(())
.delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
My guess is that you're leaking because you store an AnyCancellable in subscriptions and you never remove it.
The sink operator creates the AnyCancellable. Unless you store it somewhere, the subscription will be cancelled prematurely. But if we use the Subscribers.Sink subscriber directly, instead of using the sink operator, there will be no AnyCancellable for us to manage.
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: { _ in
withAnimation {
showRedView = false
}
}
))
}
But this is still overkill. You don't need Combine for this. You can schedule the event directly:
func fadeRedView() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
withAnimation {
showRedView = false
}
}
}

Which is better when creating multiple Publishers in Combine? AnyCancellable for each or Set<AnyCancellable> with .store(in: &self.cancellableSet)?

I'm somewhat new to Combine and reactive programming in general. I think I've come across two different ways to do the same thing, but I'm wondering why I should chose one over the other.
I have a simple model that stores and publishes values related to the Apple Watch status. Below are the two different ways I think I'm doing the same thing.
In this first approach, I'm using a separate AnyCancellable? for each Publisher:
class WatchConnectivityModel: ObservableObject {
init() {
activationState = WCSession.default.activationState
isWatchAppInstalled = WCSession.default.isWatchAppInstalled
isComplicationEnabled = WCSession.default.isComplicationEnabled
assignPublishers()
}
#Published var activationState: WCSessionActivationState
#Published var isWatchAppInstalled: Bool
#Published var isComplicationEnabled: Bool
private var activationStateStream: AnyCancellable?
private var isWatchAppInstalledStream: AnyCancellable?
private var isComplicationEnabledStream: AnyCancellable?
private func assignPublishers() {
activationStateStream = WCSession.default
.publisher(for: \.activationState)
.receive(on: RunLoop.main)
.assign(to: \.activationState, on: self)
isWatchAppInstalledStream = WCSession.default
.publisher(for: \.isWatchAppInstalled)
.receive(on: RunLoop.main)
.assign(to: \.isWatchAppInstalled, on: self)
isComplicationEnabledStream = WCSession.default
.publisher(for: \.isComplicationEnabled)
.receive(on: RunLoop.main)
.assign(to: \.isComplicationEnabled, on: self)
}
}
Here is my second approach, but instead of separate AnyCancellable? objects I'm using a single Set<AnyCancellable> along with .store(in: &self.cancellableSet) on each Publisher:
class WatchConnectivityModel: ObservableObject {
init() {
activationState = WCSession.default.activationState
isWatchAppInstalled = WCSession.default.isWatchAppInstalled
isComplicationEnabled = WCSession.default.isComplicationEnabled
assignPublishers()
}
#Published var activationState: WCSessionActivationState
#Published var isWatchAppInstalled: Bool
#Published var isComplicationEnabled: Bool
private var cancellableSet: Set<AnyCancellable> = []
private func assignPublishers() {
_ = WCSession.default
.publisher(for: \.activationState)
.receive(on: RunLoop.main)
.assign(to: \.activationState, on: self)
.store(in: &self.cancellableSet)
_ = WCSession.default
.publisher(for: \.isWatchAppInstalled)
.receive(on: RunLoop.main)
.assign(to: \.isWatchAppInstalled, on: self)
.store(in: &self.cancellableSet)
_ = WCSession.default
.publisher(for: \.isComplicationEnabled)
.receive(on: RunLoop.main)
.assign(to: \.isComplicationEnabled, on: self)
.store(in: &self.cancellableSet)
}
}
I'm guessing that the first approach would be better if I need to manually do something to one of the three specific streams, however I don't need to in this case. Other than that, is there anything that makes one of these approaches a better choice than the other? Is there anything major that I'm missing when it comes to memory management going with one over the other?
The second one seems a little weird to me because of the whole _ = part, because that seems like an extra artifact that's hard to explain: why am I assigning this whole thing to nothing? The first option avoids that possible confusion.
There is no particular reason to create a separate variable for each subscription, if all subscriptions will be destroyed at the same time.
Note also that you don't need to use a Set<AnyCancellable>. An array works just as well or better:
private var tickets: [AnyCancellable] = []
You don't need “the whole _ = part” at all. The store(in:) method returns Void, so the compiler knows the return value can be ignored.

.sink method from Combine not working on iOS 13.3

I'm using sink method to call function when variable value changed.
Code working on iOS 13.2.2 but not on iOS 13.3. Function segmentedChanged not called when segmentedSelected variable changed.
public class ChooseViewModel: ObservableObject {
#Published var segmentedSelected = Int()
init() {
_ = $segmentedSelected
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.sink(receiveValue: self.segmentedChanged(indexValue:))
}
func segmentedChanged(indexValue segIndex: Int) {
print(segIndex)
}
}
This might be due to fixed releasing of cancelable (just an assumption). Try the following
var cancellables = Set<AnyCancellable>()
init() {
$segmentedSelected
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.sink(receiveValue: self.segmentedChanged(indexValue:))
.store(in: &cancellables)
}

iOS Swift Combine: cancel a Set<AnyCancellable>

If I have stored a cancellable set into a ViewController:
private var bag = Set<AnyCancellable>()
Which contains multiple subscription.
1 - Should I cancel subscription in deinit? or it does the job automatically?
2 - If so, how can I cancel all the stored subscriptions?
bag.removeAll() is enough?
or should I iterate through the set and cancel all subscription one by one?
for sub in bag {
sub.cancel()
}
Apple says that the subscription is alive until the stored AnyCancellable is in memory. So I guess that deallocating the cancellables with bag.removeAll() should be enough, isn't it?
On deinit your ViewController will be removed from memory. All of its instance variables will be deallocated.
The docs for Combine > Publisher > assign(to:on:) say:
An AnyCancellable instance. Call cancel() on this instance when you no
longer want the publisher to automatically assign the property.
Deinitializing this instance will also cancel automatic assignment.
1 - Should I cancel subscription in deinit? or it does the job automatically?
You don't need to, it does the job automatically. When your ViewController gets deallocated, the instance variable bag will also be deallocated. As there is no more reference to your AnyCancellable's, the assignment will end.
2 - If so, how can I cancel all the stored subscriptions?
Not so. But often you might have some subscriptions that you want to start and stop on, say, viewWillAppear/viewDidDissapear, for example. In this case your ViewController is still in memory.
So, in viewDidDissappear, you can do bag.removeAll() as you suspected. This will remove the references and stop the assigning.
Here is some code you can run to see .removeAll() in action:
var bag = Set<AnyCancellable>()
func testRemoveAll() {
Timer.publish(every: 1, on: .main, in: .common).autoconnect()
.sink { print("===== timer: \($0)") }
.store(in: &bag)
Timer.publish(every: 10, on: .main, in: .common).autoconnect()
.sink { _ in self.bag.removeAll() }
.store(in: &bag)
}
The first timer will fire every one second and print out a line. The second timer will fire after 10 seconds and then call bag.removeAll(). Then both timer publishers will be stopped.
https://developer.apple.com/documentation/combine/publisher/3235801-assign
if you happened to subscribe to a publisher from your View controller, likely you will capture self in sink, which will make a reference to it, and won't let ARC remove your view controller later if the subscriber didn't finish, so it's, advisable to weakly capture self
so instead of:
["title"]
.publisher
.sink { (publishedValue) in
self.title.text = publishedValue
}
.store(in: &cancellable)
you should use a [weak self]:
["title"]
.publisher
.sink { [weak self] (publishedValue) in
self?.title.text = publishedValue
}
.store(in: &cancellable)
thus, when View controller is removed, you won't have any retain cycle or memory leaks.
Try creating a pipeline and not storing the cancellable in some state variable. You’ll find that the pipeline stops as soon as it encounters an async operation. That’s because the Cancellable was cleaned up by ARC and it was thus automatically cancelled. So you don’t need to call cancel on a pipeline if you release all references to it.
From the documentation:
An AnyCancellable instance automatically calls cancel() when deinitialized.
I test this code
let cancellable = Set<AnyCancellable>()
Timer.publish(every: 1, on: .main, in: .common).autoconnect()
.sink { print("===== timer: \($0)") }
.store(in: &cancellable)
cancellable.removeAll() // just remove from Set. not cancellable.cancel()
so I use this extension.
import Combine
typealias CancelBag = Set<AnyCancellable>
extension CancelBag {
mutating func cancelAll() {
forEach { $0.cancel() }
removeAll()
}
}
Create a Cancellable+Extensions.swift
import Combine
typealias DisposeBag = Set<AnyCancellable>
extension DisposeBag {
mutating func dispose() {
forEach { $0.cancel() }
removeAll()
}
}
In your implementation class, in my case CurrentWeatherViewModel.swift simply add disposables.dispose() to remove Set of AnyCancellable
import Combine
import Foundation
final class CurrentWeatherViewModel: ObservableObject {
#Published private(set) var dataSource: CurrentWeatherDTO?
let city: String
private let weatherFetcher: WeatherFetchable
private var disposables = Set<AnyCancellable>()
init(city: String, weatherFetcher: WeatherFetchable = WeatherNetworking()) {
self.weatherFetcher = weatherFetcher
self.city = city
}
func refresh() {
disposables.dispose()
weatherFetcher
.currentWeatherForecast(forCity: city)
.map(CurrentWeatherDTO.init)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.dataSource = nil
case .finished:
break
}
}, receiveValue: { [weak self] weather in
guard let self = self else { return }
self.dataSource = weather
})
.store(in: &disposables)
}
}

Not receiving inputs when using `.receive(on: DispatchQueue.main)`

I’m trying to change to the main thread in the downstream with .receive(on: DispatchQueue.main) but then I don’t receive inputs when using either .subscribe(:) or .sink(receiveValue:). If I don’t change threads I do receive the proper inputs.
Publisher
extension URLSessionWebSocketTask {
struct ReceivePublisher: Publisher {
typealias Output = Message
typealias Failure = Error
let task: URLSessionWebSocketTask
func receive<S>(subscriber: S) where S: Subscriber, Output == S.Input, Failure == S.Failure {
task.receive { result in
switch result {
case .success(let message): _ = subscriber.receive(message)
case .failure(let error): subscriber.receive(completion: .failure(error))
}
}
}
}
}
extension URLSessionWebSocketTask {
func receivePublisher() -> ReceivePublisher {
ReceivePublisher(task: self)
}
}
Subscriber
extension ViewModel: Subscriber {
typealias Input = URLSessionWebSocketTask.Message
typealias Failure = Error
func receive(subscription: Subscription) {}
func receive(_ input: URLSessionWebSocketTask.Message) -> Subscribers.Demand {
// Handle input here.
// When using `.receive(on:)` this method is not called when should be.
return .unlimited
}
func receive(completion: Subscribers.Completion<Error>) {}
}
Subscribe
socketTask.receivePublisher()
.receive(on: DispatchQueue.main)
.subscribe(viewModel)
socketTask.resume()
The AnyCancellable returned by subscribe<S>(_ subject: S) -> AnyCancellable will call cancel() when it has been deinitialized. Therefore if you don't save it it will be deinitialized when the calling block goes out of scope.
Out of the videos and tutorials I have seen from WWDC, how to work with this was never addressed. What I've seen is that people are drifting towards RxSwift's DisposeBag solution.
Update Beta 4:
Combine now comes with a method on AnyCancellable called: store(in:) that does pretty much what my old solution does. You can just store the AnyCancellables in a set of AnyCancellable:
var cancellables = Set<AnyCancellable>()
...
override func viewDidLoad() {
super.viewDidLoad()
...
socketTask.receivePublisher()
.receive(on: DispatchQueue.main)
.subscribe(viewModel)
.store(in: &cancellables)
}
This way the array (and all AnyCancellables) will be deinitialized when the containing class is deinitialized.
Outdated:
If you want a solution for all Cancellables that can be used in a way that flows better you could extend Cancellable as such:
extension Cancellable {
func cancel(with cancellables: inout [AnyCancellable]) {
if let cancellable = self as? AnyCancellable {
cancellables.append(cancellable)
} else {
cancellables.append(AnyCancellable(self))
}
}
}

Resources