Swift Combine how Set<AnyCancellable> works? - swift

I have ViewModel with disposable Set defined this way
class ViewModel {
private var disposables = Set<AnyCancellable>()
func sync() {
repo.syncObjects()
.handleEvents(receiveCancel: {
print("Synced objects: CANCELED!")
})
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Synced objects: \(error)")
case .finished:
print("Synced objects: finished")
}
}) { objects in
print("Synced objects: \(objects)")
}.store(in: &disposables)
}
deinit { print("ViewModel deinit") }
}
I am calling sync() in onAppear in SwiftUI view. Then I am fast switching screens and ViewModel referenced from SwiftUI view is deallocated by ARC like deinit is called but subscriptions seems to remain alive and disposable reference does not cancel subscription it fetches data from Network and saved them in Core Data and prints Synced objects: objects, Synced objects: finished. And keeps being alive even when I stop switching screens for several seconds to complete old requests.
Should I manually cancel AnyCancellable? shouldn't it be cancelled automagically?

No, you dont need to cancel any cancellable because this kind of object calls the method cancel when deinitialized. So your code is correct.
Apple's documentation of AnyCancellable:
An AnyCancellable instance automatically calls cancel() when deinitialized
https://developer.apple.com/documentation/combine/anycancellable

Related

Why does this NOT leak memory? RxFeedback

class ViewModel {
...
func state(with bindings: #escaping (Driver<State>) -> Signal<Event>) -> Driver<State> {
Driver.system(
initialState: .initial,
reduce: State.reduce(state:event:),
feedback:
bindings,
react(request: { $0.startLoading }, effects: { _ in
self.fetchFavoriteRepositoriesUseCase.execute()
.asObservable()
.observe(on: self.scheduler)
.map(self.repositoriesToRepositoryViewModelsMapper.map(input:))
.map { repositories in .loaded(repositories) }
.asSignal { error in
.just(.failed(error.localizedDescription))
}
}))
}
...
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let initialTrigger = BehaviorRelay<Void>(value: ())
let trigger = Observable.merge(initialTrigger.asObservable(), refreshRelay.asObservable())
let uiBindings: (Driver<FavoriteRepositoriesViewModel.State>) -> Signal<FavoriteRepositoriesViewModel.Event> = bind(self) { me, state in
let subscriptions = [
state.drive(onNext: { state in
switch state {
case .initial:
print("Initial")
case .loading:
print("Loading")
case .failed:
print("Failed")
case .loaded:
print("Loaded")
}
})
]
let events: [Signal<FavoriteRepositoriesViewModel.Event>] = [
trigger.map {
.load
}
.asSignal(onErrorSignalWith: .empty())
]
return Bindings(subscriptions: subscriptions, events: events)
}
viewModel.state(with: uiBindings)
.drive()
.disposed(by: disposeBag)
}
}
I'm trying to grasp my head around why the react method from RxFeedback does NOT create a memory leak in this case. It has the effects closure as one of its arguments which is an #escaping closure and I'm not weakifying it, but capturing self strongly in it to call the use case. I assume it has nothing to do with RxFeedback but my knowledge of ARC and memory management.
To test the deallocation of the ViewController I'm just popping it from a NavigationController.
I would appreciate a detailed explanation on why this code is NOT creating a retain cycle. Thanks in advance!
There is no retain cycle. However, your view controller is holding several references (both direct and indirect) to your view model.
So for example, your view controller has a viewModel property. It's also holding a disposeBag which is retaining a disposable, which retains an Observable that retains the closure in your view model, which retains the view model.
The only time the strong capture of self is an issue is if the disposable is also being retained by the same object that is being captured. In this case, the view model is "self" but the view controller is the one retaining the disposable (through its dispose bag.)

Weakly captured self won't let the view model deallocate until the Task finishes

I am trying to learn ARC and I'm having a hard time with a weakly captured self. My project is using MVVM with SwiftUI. I'm presenting a sheet (AuthenticationLoginView) that has a #StateObject var viewModel = AuthenticationLoginViewModel() property. On dismiss of the presented sheet, I expect that the viewModel will have it's deinit called and so it does until I run an asynchronous function within a Task block.
class AuthenticationLoginViewModel: ObservableObject {
#Published var isLoggingIn: Bool = false
private var authenticationService: AuthenticationService
private var cancellables: Set<AnyCancellable> = Set()
private var onLoginTask: Task<Void, Never>?
init(authenticationService: AuthenticationService) {
self.authenticationService = authenticationService
}
deinit {
onLoginTask?.cancel()
LoggerService.log("deallocated")
}
public func onLogin() {
guard !isLoggingIn else { return }
isLoggingIn = true
onLoginTask = Task { [weak self] in
await self?.login()
}
}
private func login() async {
LoggerService.log("Logging in...")
sleep(2)
//
// self is still allocated here <<<---- ???
//
let authResponse = try? await self.authenticationService.authenticate(username: username, password: password)
LoggerService.log(self.isLoggingIn) // <<--- prints `true`
handleLoginResponse(authResponse: authResponse)
}
}
So I have my two cases here:
Case #1
I present the sheet.
I dismiss the sheet.
The deinit function is getting called (app logs: "deallocated")
Case #2
I present the sheet.
I press the login button so the onLogin function is getting called.
I dismiss the sheet before the sleep(2) ends.
---- I EXPECT the "deallocated" message to be printed from deinit and the logging at LoggerService.log(self.isLoggingIn) to print nil and the self.authenticationService.authenticate(... to never be called as self doesn't exist anymore.
Not expected but happening: the app prints "Logging in", sleeps for 2 seconds, calls the service, prints true, and then deallocates the view model (the view was dismissed 2 seconds ago)
What am I doing wrong?
I'm still learning and I'm pretty much unsure if this is normal or I miss something. Anyway, I expect the view model to be deallocated as the view referencing it was dismissed.
At the time you call onLogin the reference to self is valid and so the Task commences.
After that, the reference to self in login keeps self alive. The Task has a life of its own, and you did not cancel it.
Moreover the use of sleep is wrong, as it is not cancellable in any case, so neither is your Task. Use Task.sleep.

Why does this combine subscription not deallocate in custom ViewModifier?

In the documentation for assign it says the following...
The Subscribers/Assign instance created by this operator maintains a
strong reference to object, and sets it to nil when the upstream
publisher completes (either normally or with an error).
In the ViewModifier below the assign method in subscribeToKeyboardChanges() refers to self but self is a struct here so there's no way it can create a strong reference
Why doesn't the subscription in subscribeToKeyboardChanges() get immediately deallocated?
What is the actually happening here behind the scenes?
struct KeyboardHandler: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, self.keyboardHeight)
.animation(.default)
.onAppear(perform: subscribeToKeyboardChanges)
}
private let keyboardWillShow = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
.map { $0.height }
private let keyboardWillHide = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat.zero }
private func subscribeToKeyboardChanges() {
_ = Publishers.Merge(keyboardWillShow, keyboardWillHide)
.subscribe(on: DispatchQueue.main)
.assign(to: \.self.keyboardHeight, on: self)
}
}
I believe you`re referring to the wrong function description. Here is right one:
assign(to:on:)
Assigns a publisher’s output to a property of an object.
...
Return Value
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.
So in your subscribeToKeyboardChanges example code it's expected that subscription will be canceled after function finishes. You have to keep strong reference to AnyCancellable return from assign to keep subscription in memory.
EDIT:
It appears that in this line assign copies self and holds it in the memory until cancel() call.
.assign(to: \.self.keyboardHeight, on: self)
Therefore View that uses KeyboardHandler view modifier will never be deallocated with subscription and will eventually bloat the memory during navigation. For example, here is the screenshot after 3 navigations see 3 instances of KeyboardHandler still in the memory.

OnNext not called when disposing the result of Observable.subscribe

I'm subscribing to an observable, but if I'm adding the disposable to the DisposeBag in my class, the onNext block is never called.
Here is my code :
#objc class AppleMusicPlaylistManager: NSObject {
let disposeBag = DisposeBag()
let playlists: [MPMediaPlaylist] = []
func importAppleMusicPlaylist() {
playlists.forEach { applePlaylist in
applePlaylist.getItunesStoreTracks().subscribe(onNext: { tracks in
// Doing things here
}).addDisposableTo(disposeBag)
}
}
}
where getItunesStoreTracks return a RxSwift.Observable<[SoundsMusicITunesStore]> and the whole thing is used like that AppleMusicPlaylistManager().importAppleMusicPlaylist()
It all works as expected.
Current logic with disposeBag points out, that the observables will not be disposed of until the disposeBag is alive.
In your case - AppleMusicPlaylistManager().importAppleMusicPlaylist(), you create a manager and then you call the async requests, while the manager is deallocating. Thus all observables are deallocating as well.
In order for this to work correctly, you either have to set this manager as shared and use this method: AppleMusicPlaylistManager.shared.importAppleMusicPlaylist() or save this manager to some property in order to not deallocate immediately.

Retaining a Signal or SignalProducer?

Is it my responsibility to maintain a reference to a Signal or a SignalProducer, e.g., using an instance variable?
In other words, will they automatically disappear and stop sending events when they get deallocated?
FYI, not necessary, the Signal will be disposed and stop forwarding events.
Signalis a class type, if no one have a reference to it, it should be deinit.
However, Signal implementation introduces a tricky way to retain itself, see state property, so that there exists some memory leaks in temporary. As seen in source code, if there have some observers subscribe on the Signal, it's state does retain it in turn until all observers unsubscribed or the Signal received completed/error/interrupted event.
Here some marked code snippets.
// definition of SignalState
private struct SignalState<Value, Error: Swift.Error> {
var observers: Bag<Signal<Value, Error>.Observer> = Bag()
var retainedSignal: Signal<Value, Error>? // here is the key
}
public func observe(_ observer: Observer) -> Disposable? {
var token: RemovalToken?
state.modify {
$0?.retainedSignal = self // retain self when one observer on
token = $0?.observers.insert(observer)
}
if let token = token {
return ActionDisposable { [weak self] in
if let strongSelf = self {
strongSelf.state.modify { state in
state?.observers.remove(using: token)
if state?.observers.isEmpty ?? false {
// break retain cycle when disposed
state!.retainedSignal = nil
}
}
}
}
} else {
observer.sendInterrupted()
return nil
}
}
How about SignalProducer?
It is really intuitive, SignalProducer is just struct type, and you should not consider its lifetime.