RxSwift Disposing one subscription invokes dispose of another subscription - rx-swift

I have a PublishSubject<InfoData> in a ViewController. And I subscribe to it, so when it emits an event - I show the UIAlertViewController.
let infoData = PublishSubject<InfoData>()
private func bindInfoData() {
infoData.subscribe(onNext: { [weak self] (title, message) in
self?.presentInfoSheetController(with: title, message: message)
}).disposed(by: disposeBag)
}
In a ViewController I have a tableView with section headers. Section header view has a infoMessageAction: PublishSubject<InfoData?>. When initiating a view for viewForHeaderInSection I make a subscription between the infoMessageAction and infoData.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = FutureSpendingsHeaderView(frame: frame)
view.infoMessageAction
.compactMap { $0 }
.bind(to: infoData)
.disposed(by: view.disposeBag)
return view
}
When the header view initiated for the first time all works good - infoMessageAction triggers the infoData which in turn triggers presentation of AlertViewController.
When I scroll header view beyond the screen the subscription between view.infoMessageAction and infoData disposes (which is expected behavior as the view was deinited).
But I get disposed the subscription between infoData and ViewController as well. I receive event completed and dispose for view.infoMessageAction <-> infoData subscription and also event completed and dispose for infoData <-> ViewController subscription.
I expect that only view.infoMessageAction <-> infoData subscription should break. Also both subscriptions disposed by different disposeBag. Why is infoData <-> ViewController subscription get disposed and how to prevent it?
Thanks in advance!

When your FutureSpendingsHeaderView is deinitialized, whatever view that is the source of infoMessageAction is also being deinitialized, and that view emits a completed event at that time. That completed event is passed on to infoData which then emits its own completed event.
Once an Observable has emitted a completed event, it is done. It can't emit any more events. So the subscription to it is disposed.
Your answer #Alex changes the equation by changing the order that things inside your view get deinitialized. The disposeBag is getting deinitialized first now, which breaks the observable chain before the view sends the completed event.
A better solution would be to use a PublishRelay rather than a PublishSubject. Relays don't emit completed events.
Even better than that would be to get rid of the subject entirely and do something like:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = FutureSpendingsHeaderView(frame: frame)
view.infoMessageAction
.compactMap { $0 }
.subscribe(onNext: { [weak self] (title, message) in
self?.presentInfoSheetController(with: title, message: message)
})
.disposed(by: view.disposeBag)
return view
}

Found the problem, if anyone faces such situation.
In section header view I initiated disposeBag as constant and thought that everything else is handled by RxSwift itself when the view is deinited.
So I updated the view to:
var disposeBag = DisposeBag()
deinit {
disposeBag = DisposeBag()
}
Now the subscription get disposed as needed.

Related

Not understanding how to implement .drive(onNext: rxcocoa

// View controller call
viewModel.bindNotificationReadEvents(readNotificationID: readNotificationIDPublisher.asDriver(onErrorDriveWith: .empty()))
viewModel.reloadDataSourceForNotificationReadEvent.drive(reloadDataSourceForNotificationReadEventBinder).disposed(by: rx.disposeBag)
// View model
var reloadDataSourceForNotificationReadEvent: Driver<[NotificationItem]> = .empty()
fileprivate let dataSourceRelay = BehaviorRelay<[NotificationItem]>(value: [])
public func bindNotificationReadEvents(readNotificationID: Driver<String>) {
readNotificationID.drive(onNext: { [weak self] notificationID in
// read notification IDs on User Defaults
UserDefaults.main?.unreadNotificationIDs.append(notificationID)
// Update data source relay
self?.reloadDataSourceForNotificationReadEvent = readNotificationID.withLatestFrom(self?.dataSourceRelay.asDriver() ?? .empty())
}).disposed(by: rx.disposeBag)
}
when this method is called from the viewcontroller in the viewmodel it just skips both the lines and nothing executes when i checked it while debugging neither is the userdefaults updated nor is data source relay can someone please help me out.

Why do we need to call "disposeBy(bag)" explicitly after "subscribe" in RxSwift

I read about this from a blog post http://adamborek.com/memory-managment-rxswift/:
When you subscribe for an Observable the Disposable keeps a reference to the Observable and the Observable keeps a strong reference to the Disposable (Rx creates some kind of a retain cycle here). Thanks to that if user navigates back in navigation stack the Observable won’t be deallocated unless you want it to be deallocated.
So purely just for understanding this, I created this dummy project: where is there a view, and in the middle of the view, there is a giant button which will emit events about how many times the button is tapped on. Simple as that.
import UIKit
import RxCocoa
import RxSwift
class Button: UIButton {
private var number: Int = 0
private let buttonPushedSubject: PublishSubject<Int> = PublishSubject.init()
var buttonPushedObservable: Observable<Int> { return buttonPushedSubject }
deinit {
print("Button was deallocated.")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
#objc final func buttonTapped() {
number = number + 1
buttonPushedSubject.onNext(number)
}
}
class ViewController: UIViewController {
#IBOutlet private weak var button: Button!
deinit {
print("ViewController was deallocated.")
}
override func viewDidLoad() {
super.viewDidLoad()
button.buttonPushedObservable.subscribe(onNext: { (number) in
print("Number is \(number)")
}, onError: nil, onCompleted: nil, onDisposed: nil)
}
}
And surprisingly, after I close this view controller, the logs look like this:
Number is 1
Number is 2
Number is 3
Number is 4
ViewController was deallocated.
Button was deallocated.
...
which means both ViewController and the Button have been released! In this case, I didn't call the disposeBy(bag) and the compiler giving warning.
Then I started looking at the implementation of subscribe(onNext:...) (c/p below):
let disposable: Disposable
if let disposed = onDisposed {
disposable = Disposables.create(with: disposed)
}
else {
disposable = Disposables.create()
}
let callStack = Hooks.recordCallStackOnError ? Hooks.customCaptureSubscriptionCallstack() : []
let observer = AnonymousObserver<E> { event in
switch event {
case .next(let value):
onNext?(value)
case .error(let error):
if let onError = onError {
onError(error)
}
else {
Hooks.defaultErrorHandler(callStack, error)
}
disposable.dispose()
case .completed:
onCompleted?()
disposable.dispose()
}
}
return Disposables.create(
self.asObservable().subscribe(observer),
disposable
)
In this block of code above, it is true that observer holds a strong reference to disposable through the lambda function. However, what I don't understand is that how does disposable holds a strong reference to observer?
While the observable is active there is a reference cycle, but the deallocation of your button sends a complete event which breaks the cycle.
That said, if you do something like this Observable<Int>.interval(3).subscribe() the stream will not deallocate.
Streams only shutdown (and thus deallocate) if the source completes/errors or if dispose() is called on the resulting disposable. With the above line of code, the source (interval) will never complete or error and no reference to the disposable was kept so there is no way to call dispose() on it.
The best way to think of it is this... complete/error is the source's way of telling the sink that it is done emitting (which means the stream is no longer needed,) and calling dispose() on the disposable is the sinks way of telling the source that it isn't interested in receiving any more events (which also means the stream is no longer needed.) In order to deallocate the stream, either the source or sink needs to report that it's finished.
To explicitly answer your question... You don't need to add the disposable to a dispose bag, but if the view controller deletes without disposing and the source doesn't send a complete message, the stream will leak. So safety first, make sure the sink disposes when it's done with the stream.

I want to make a tableView method static

I am currently writing a small application that involves a tableView and an array of ManagedObjects for persistent storage.
What I want to do delete all the ManagedObjects in the array by clicking a button in another view controller.
To do this, I tried to make the array a static variable, unfortunately this conflicts with the methods that I use to populate the table with data from this array. Frustrating stuff.
Here is the code for the class:
class ClassOverviewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
static var subjects = [NSManagedObject]()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return subjects.count
}
static func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell")
let subject = subjects[indexPath.row]
cell!.textLabel!.text = subject.valueForKey("subjectName") as? String
return cell!
}
static func clearSubjects() {
for item in (self.subjects)
{
CalculateClass.managedContext.deleteObject(item)
}
do {
try CalculateClass.managedContext.save()
}
catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
}
I have removed functions from the class that I did not think were necessary to show you.
It does not like me making the second tableView method static because I have taken that method from UITableViewDataSource.
I am unsure how I am supposed to proceed. Please help!
Go back to the non-static implementation so your table works.
When you want to remove the objects either:
get a reference to your ClassOverviewController object and call its method
or, if there's no connection between controllers, use a notification that tells the ClassOverviewController object that it should reset its array.

Add completion handler to presentViewControllerAsSheet(NSViewController)?

I am attempting to present a sheet configuration view (AddSoundEffect) for my main window/view controller (I'm using storyboards), and when the configuration view controller is dismissed, take the values entered in the AddSoundEffect view and pass that back to the main view. My current code in the main view controller:
presentViewControllerAsSheet(self.storyboard!.instantiateControllerWithIdentifier("AddSoundEffect") as! AddSoundViewController
And in the AddSoundViewController.swift file, the code to dismiss it is:
self.dismissViewController(self)
To pass the data, I have a class-independent tuple that I save data to. How do I add a completion handler to presentViewControllerAsSheet, and (optionally) is there a better way to pass the data between view controllers?
Setup: Xcode version 6.4, OS X 10.10.4
Delegation pattern is the easiest way for you.
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
protocol AddSoundViewControllerDelegate: class {
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect)
}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
weak var delegate: AddSoundViewControllerDelegate?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.delegate?.soundViewController(self, didAddSoundEffect: soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController, AddSoundViewControllerDelegate {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.delegate = self
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect) {
// This method is called only when new sound effect is added
}
}
Another way is to use closures:
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
var completionHandler: ((SoundEffect) -> ())?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.completionHandler?(soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.completionHandler = { [weak self] (soundEffect) -> () in
// Called when new sound effect is added
}
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
}
Or many other ways like sending notification, ... Whatever suits your needs. But delegation pattern or closures is the best way to go in this specific case.
I missed that your question is about NSViewController. This example is for iOS, but same pattern can be used on OS X without any issues.
The easiest way to detect sheet opening or closing is to use the Sheet Notifications:
class ViewController: NSViewController, NSWindowDelegate {
override func viewDidLoad(){
NSApplication.sharedApplication().windows.first?.delegate = self
}
func windowDidEndSheet(notification: NSNotification) {
}
func windowWillBeginSheet(notification: NSNotification) {
}
}

Progress Bar in Cocoa

I have a very simple application that contains a WebView. This webview loads an HTML5 app and it takes some time while the content is being built inside the webview.
I would like to show a progress bar until the content finishes loading and show the webview when the content is ready. It takes roughly 10 seconds.
??
Swift 2.2
override func viewDidLoad() {
super.viewDidLoad()
webView.frameLoadDelegate = self
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .New, context: nil) // add observer for key path
}
/// Observer listening for progress changes
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if (keyPath == "estimatedProgress") { // listen to changes and updated view
if self.webView.estimatedProgress == 0 { return }
// All UI operations always in main thread
dispatch_async(dispatch_get_main_queue(), {
// *100 because progressIndicator in Mac OS wants values from 0 to 100
self.progressIndicator.doubleValue = self.webView.estimatedProgress * 100
})
}
}
/// Hide progress indicator on finish
func webView(sender: WebView!, didFinishLoadForFrame frame: WebFrame!) {
dispatch_async(dispatch_get_main_queue(), { self.progressIndicator.hidden = true })
}
/// Show progress indicator on start page loading
func webView(sender: WebView!, didStartProvisionalLoadForFrame frame: WebFrame!) {
dispatch_async(dispatch_get_main_queue(), { self.progressIndicator.hidden = false })
}
Swift 3
override func viewWillAppear() {
super.viewWillAppear()
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") { // listen to changes and updated view
DispatchQueue.main.async {
// Here you can do set your actions on progress update
// E.g.: someProgressBar.doubleValue = self.webView.estimatedProgress * 100
}
}
}
override func viewWillDisappear() {
super.viewWillDisappear()
webView.removeObserver(self, forKeyPath: "estimatedProgress")
}
Full solution
I've created Swift 3 Cocoa code snippet with NSViewController with embedded WKWebViewController and NSProgressIndicator so you can look at live example.
You need to create a class that conforms to the WebFrameLoadDelegate protocol and set it as the delegate for your WebView.
Delegates in Cocoa are its callback pattern. You make a class that conforms to a protocol, implementing the required messages and whatever optional messages you need, add that class as a delegate to the main class, in your case the WebView, and your delegate gets messages whenever things happen in the main class.
From your delegate you could create a timer that repeats ever 1/10th of a second and sends the main WebView the message - (double)estimatedProgress to update your progress bar. Once the view is loaded invalidate the timer and remove the progress bar.
WebKit posts WebViewProgressEstimateChangedNotification and friends to give you this information

Resources