Why does this combine subscription not deallocate in custom ViewModifier? - swift

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.

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.)

Multiple UITextFields and textDidChangeNotification notification

I was playing with Combine framework lately and was wondering if it is possible to create some smart extension to get text changes as Publisher.
Let's say I've got two UITextFields:
firstTextField.textPub.sink {
self.viewModel.first = $0
}
secondTextField.textPub.sink {
self.viewModel.second = $0
}
where first and second variable is just `#Published var first/second: String = ""
extension UITextField {
var textPub: AnyPublisher<String, Never> {
return NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification)
.map {
guard let textField = $0.object as? UITextField else { return "" }
return textField.text ?? ""
}
.eraseToAnyPublisher()
}
}
This doesn't work because I'm using shared instance of NotificationCenter so when I make any change to any of textFields it will propagate new value to both sink closures. Do you think is there any way to achieve something similar to rx.text available in RxSwift? I was thinking about using addTarget with closure but it would require using associated objects from Objective-C.
I figured this out. We can pass object using NotificationCenter and then filter all instances that are not matching our instance. It seems to work as I expected:
extension UITextField {
var textPublisher: AnyPublisher<String, Never> {
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: self)
.compactMap { $0.object as? UITextField }
.map { $0.text ?? "" }
.eraseToAnyPublisher()
}
}
I would suggest you add subscribers to the view modal, and connect them a text field publisher within the context of the view controller.
NotificationCenter is useful to dispatch events app-wide; there's no need to use it when connecting items that are fully owned by the View Controller. However, once you've updated the view modal it may make sense to publish a 'View Modal Did Change' event to NotificationCenter.

Observe a NSManagedObject in Swift 4 can cause crashes when modified in another thread?

What I have:
a NSManagedObject that sets a dynamic property to true when it's deleted from CoreData
override func prepareForDeletion() {
super.prepareForDeletion()
hasBeenDeleted = true
}
And within a view, I observe this NSManagedObject with the new Observe pattern of Swift 4
// I added this to observe the OBSERVED deletion to avoid a crash similar to:
// "User was deallocated while key value observers were still registered with it."
private var userDeletionObserver: NSKeyValueObservation?
private func observeUserDeletion() {
userDeletionObserver = user?.observe(\.hasBeenDeleted, changeHandler: { [weak self] (currentUser, _) in
if currentUser.hasBeenDeleted {
self?.removeUserObservers()
}
})
}
private func removeUserObservers() {
userDeletionObserver = nil
userObserver = nil
}
private var userObserver: NSKeyValueObservation?
private var user: CurrentUser? {
willSet {
// I remove all observers in willSet to also cover the case where we try to set user=nil, I think it's safer this way.
removeUserObservers()
}
didSet {
guard let user = user else { return }
// I start observing the NSManagedObject for Deletion
observeUserDeletion()
// I finally start observing the object property
userObserver = user.observe(\.settings, changeHandler: { [weak self] (currentUser, _) in
guard !currentUser.hasBeenDeleted else { return }
self?.updateUI()
})
}
}
So now, here come one observation and the question:
Observation: Even if I don't do the observeUserDeletion thing, the app seems to work and seems to be stable so maybe it's not necessary but as I had another crash related to the observe() pattern I try to be over careful.
Question details: Do I really need to care about the OBSERVED object becoming nil at any time while being observed or is the new Swift 4 observe pattern automatically removes the observers when the OBSERVED object is 'nilled'?

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.

Swift: possible removeObserver cyclic reference in addObserverForName's usingBlock

I'm toying around with a small Swift application. In it the user can create as many MainWindow instances as he wants by clicking on "New" in the application's menu.
The application delegate holds an array typed to MainWindowController. The windows are watched for the NSWindowWillCloseNotification in order to remove the controller from the MainWindowController array.
The question now is, if the removal of the observer is done correctly – I fear there might be a cyclic reference to observer, but I don't know how to test for that:
class ApplicationDelegate: NSObject, NSApplicationDelegate {
private let notificationCenter = NSNotificationCenter.defaultCenter()
private var mainWindowControllers = [MainWindowController]()
func newWindow() {
let mainWindowController = MainWindowController()
let window = mainWindowController.window
var observer: AnyObject?
observer = notificationCenter.addObserverForName(NSWindowWillCloseNotification,
object: window,
queue: nil) { (_) in
// remove the controller from self.mainWindowControllers
self.mainWindowControllers = self.mainWindowControllers.filter() {
$0 !== mainWindowController
}
// remove the observer for this mainWindowController.window
self.notificationCenter.removeObserver(observer!)
}
mainWindowControllers.append(mainWindowController)
}
}
In general you should always specify that self is unowned in blocks registered with NSNotificationCenter. This will keep the block from having a strong reference to self. You would do this with a capture list in front of the parameter list of your closure:
{ [unowned self] (_) in
// Block content
}