RxSwift PublishSubject is being disposed - swift

I bind Button pressed to PublishSubject in a router like so:
hambugerButton
.rx_tap
.bindTo(router.openMenu)
.addDisposableTo(disposeBag)
In my Router:
let openMenu = PublishSubject<Void>()
//...
openMenu
.map { _ in
menuNavigationController
}
.bindTo(mainNavigationController.rx_present())
.addDisposableTo(disposeBag)
However, when the controller is being deallocated, the button is sending 'complete' signal. When PublishSubject receives it, it won't react to signals from another controller (which is understandable: it is an Observable guarantee).
The only solution I came up with:
hambugerButton
.rx_tap
.subscribeNext {
self.router.openMenu.onNext()
}
.addDisposableTo(disposeBag)
Which looks ugly and kinda spoils the idea of a reactive interface.
My question is, is there a way to avoid propagation of the Completed event to PublishSubject? Can I make some Observer which will ignore such events?

If the view controller which owns the hamburgerButton is being deallocated, and thus the hamburgerButton is also being deallocated, why wouldn't you want the binding to router.openMenu to also be deallocated? Maybe it's not clear what your view controller hierarchy is from your question.
Also, in the first snippet, you shouldn't be making a binding without adding it to a DisposeBag like so:
hambugerButton
.rx_tap
.bindTo(router.openMenu)
.addDisposableTo(disposeBag)

Related

Is calling `disposed(by:)` necessary in any case?

As the title, is it necessary to call disposed(by:) in any case? If yes, why?
Consider a simple example like this:
class ViewController: UIViewController {
let button = UIButton()
override func viewDidLoad() {
button.rx.tap.bind(onNext: { _ in
print("Button tapped!")
})
// Does this make any retain cycle here?
}
}
No, it is not necessary to call .disposed(by:) in every case.
If you have an Observable that you know will eventually send a stop event, and you know that you want to keep listening to that observable until it does so, then there is no reason/need to dispose the subscription and therefore no need to insert the disposable into a dispose bag.
The reason .subscribe and its ilk return a Disposable is so that the calling code can end the subscription before the observable has completed. The calling code ends the subscription by calling dispose() on the disposable returned. Otherwise, the subscription will continue until the source observable sends a stop event (either completed or error.)
If the calling code doesn't dispose the subscription, and the source observable doesn't send a stop event, then the subscription will continue to operate even if all other code involved has lost all references to the objects involved in the subscription.
So for example, if you put this in a viewDidLoad:
_ = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.subscribe(onNext: { print($0) })
The code above will continue to print values long after the view controller that created it ceases to exist.
In the example you presented, the UIButton object will emit a completed event when it is deinitialized, so if you want to listen to the button right up until that happens, putting the disposable in a dispose bag isn't necessary.
Ignoring disposables means you need to be very cognizant as to which Observables complete and which ones don't, but if you have that understanding, you can ignore them. Just remember that the next developer down the line, or future you, won't have as good an understanding of the code as you do.

MacOS Quartz Event Tap listening to wrong events

I am trying to intercept mouse move events using the CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) method as shown below:
let cfMachPort = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
place: CGEventTapPlacement.headInsertEventTap,
options: CGEventTapOptions.defaultTap,
eventsOfInterest:CGEventMask(CGEventType.mouseMoved.rawValue),
callback: {(eventTapProxy, eventType, event, mutablePointer) -> Unmanaged<CGEvent>? in event
print(event.type.rawValue) //Breakpoint
return nil
}, userInfo: nil)
let runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort!, 0)
let runLoop = RunLoop.current
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddSource(cfRunLoop, runloopSource, CFRunLoopMode.defaultMode)
I pass as event type eventsOfInterest mouseMoved events with a raw value of 5 as seen in the documentation. But for some reason my print() is not executed unless I click with the mouse. Inspecting the send mouse event in the debugger gives me a raw value of 2, which according to the documentation is a leftMouseUp event.
In the documentation for CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) it says:
Event taps receive key up and key down events [...]
So it seems like the method ignores mouseMoved events in general?! But how am I supposed to listen to mouseMoved events? I am trying to prevent my cursor (custom cursor) from being replaced (for example when I hover over the application dock at the bottom of the screen).
You need to bitshift the CGEventType value used to create the CGEventMask parameter. In Objective-C, there is a macro to do this: CGEventMaskBit.
From the CGEventMask documentation:
to form the bit mask, use the CGEventMaskBit macro to convert each constant into an event mask and then OR the individual masks together
I don't know the equivalent mechanism in swift; but the macro itself looks like this:
*/ #define CGEventMaskBit(eventType) ((CGEventMask)1 << (eventType))
In your example, it's sufficient to just manually shift the argument; e.g.
eventsOfInterest:CGEventMask(1 << CGEventType.mouseMoved.rawValue),
I would point out that the code example given in the question is a little dangerous; as it creates a default event tap and then drops the events rather than allowing them to be processed. This messes up mouse click handling and it was tricky to actually terminate the application using the mouse. Anyone running the example could set the event tap type to CGEventTapOptions.listenOnly to prevent that.
Here is a way to listen for mouseMove global events (tested with Xcode 11.2+, macOS 10.15)
// ... say in AppDelegate
var globalObserver: Any!
var localObserver: Any!
func applicationDidFinishLaunching(_ aNotification: Notification) {
globalObserver = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("in background: {\(location.x), \(location.y)}")
}
localObserver = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("active: {\(location.x), \(location.y)}")
return event
}
...
There's another thing incorrect in your code, although you might be lucky and it isn't normally causing a problem.
As documented for the mode parameter to CFRunLoopAddSource: "Use the constant kCFRunLoopCommonModes to add source to the set of objects monitored by all the common modes."
That third parameter should instead be CFRunLoopMode.commonModes.
What you have, CFRunLoopMode.defaultMode aka kCFRunLoopDefaultMode, is instead for use when calling CFRunLoopRun.

Looping through entities which describe what action should be taken on screen after keypresses

Please forgive me if I don't describe this question too well, I am new to programming MacOS apps using Swift. I know the way I'm going about this is probably wrong and I just need someone to tell me the right way.
My main app screen
I have a Core Data application that stores an ordered list of entities called Items. These Items are intended to describe a single step in an activity that describes what should happen on screen. If you know the Mac application QLab each Item is like a single cue in QLab.
I have created an Activity class that is designed to read through each Item to determine the Item type and it's related information. Once the Item type has been determined the Activity class needs to present a View with information related to that particular Item and then wait until the user presses the right arrow key to then proceed to the next Item in the Core Data store where the process repeats until all Items have been read. Each time a new Item is read in the loop, the information on the screen should change after the user presses the right arrow each time.
The problem is that I don't know exactly how the best way of going about this should be programatically speaking. I have the code that retrieves the array of Items as an NSFetchRequest:
let moc = (NSApplication.shared.mainWindow?.contentViewController?.representedObject as! NSPersistentDocument).managedObjectContext!
let fetchRequest : NSFetchRequest = Item.fetchRequest()
do {
let items = try moc.fetch(fetchRequest)
print("Found " + String(items.count) + " items to use in the activity.")
for item in items {
print(item.itemType)
// How do I pause this loop for a user keypress after using data from this Item to display?
}
} catch {
print("Error retrieving Items")
}
I can retrieve the keydown event using NSEvent.addLocalMonitorForEvents(matching: .keyDown) and I'm also able to create View Controllers to display the information on a second screen. I just don't know how I should create the 'main loop', so to speak, so that information is displayed and then the app waits until the user presses a key to proceed...
I can share my project code if more information is needed and many thanks to anyone who can enlighten me... :)
You could try using a NSPageController. In your NSPageController you add a ContainerView which will display the ViewControllers that display information for each item. Each ViewController will need a storyboard identifier, e.g. ViewControllerItem1.
Your NSPageController class must conform to the NSPageControllerDelegate protocol and contains an array of ViewControllers to display.
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
arrangedObjects = ["ViewControllerItem1", "ViewControllerItem2", "...","ViewControllerItemN" ]
}
Note about arrangedObjects from the NSPageController documentation: An array containing the objects displayed in the page controller’s view.
Then you implement NSPageControllers viewControllerForIdentifier to return the ViewController that you currently want to display in the ContainerView.
func pageController(_ pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
switch identifier {
case "ViewControllerItem1":
return mainStoryboard().instantiateController(withIdentifier:"ViewControllerItem1") as? ViewControllerItem1
case "...":
default:
}
}
In your action handler for the key down event you implement.
self.navigateForward(sender) or self.navigateBack(sender)
I also implemented this method but I don't remember whether it was required.
func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
self.completeTransition()
}

Using "ViewState" in RxSwift/MVVM

This question is very broad but I'm not sure which aspect of it I should focus on. I have a goal to abstract away the recurring patterns of my screens such as errors, loading, empty data. The views that represent these states will not change much between the many screens I have. Perhaps they could be parameterized to allow that flexibility (e.g. showError(message: "404")).
I liked this article as a method of encapsulating the reusable UI aspects of this.
But it appears to work in an imperative context. So I have an API call and I can showError and in the response I can hideError. Thats all fine.
Now I use an RxSwift/MVVM approach where each screen binds to inputs and outputs. And I like to simplify the state my screen knows about by using a "View State" concept.
As you can see in this snippet, I can reduce a lot of logic a single Observable that the view renders.
let getFoos: (String) -> Observable<FooViewStateState> = { query in
fooService.perform(query)
.map { results in
if results.isEmpty {
return ViewState.noResults(query: query)
} else {
return ViewState.matches(query: query, results: results.map { $0.name })
}
}
.startWith(ViewState.searching(query))
}
The problem is that by using an enum ViewState its now unclear to me how to use the imperative API from before "showLoading / hideLoading ... showError / hideError, etc..." when I'm switching on the cases of this enum. If the ViewState Observable emits .loading I'd have to hide the error screen, hide the empty screen, etc..

Eureka PushRow on a modal, view won't close after item selection

I am using a SplitViewController and on the detail page (which is set to "defines context"), the user can select "+" in the navbar and I present the next view controller "modally over current context" using a segue. On that view controller I am using Eureka and one of the rows I want to use is PushRow. The issue I am running into is when I select an option on the PushRow, the view (table of options to choose that Eureka generated) never closes. The list of options stays full screen. I can see that PushRow.onChange is called and it has the correct value. For some reason that topmost view will not close.
I dug deeper and it seems like I need to modify the PushRow presentationMode to be "presentModally" since I am presenting it from a modal. However, I am not sure what to put for the controllerProvider. Is this the right path? If so, what would the correct syntax be? I also tried doing a reload in the onChange but that didn't make a difference.
private func getGroupPushRow() -> PushRow<String> {
return PushRow<String>() {
$0.title = "Group"
$0.selectorTitle = "What is the Group?"
$0.noValueDisplayText = "Select a Group..."
$0.options = self.getGroups()
$0.presentationMode = PresentationMode.presentModally(controllerProvider: ControllerProvider<VCType>, onDismiss: { () in
})
$0.onChange({ (row) in
print("in onchange \(row.value)")
// row.reload()
// self.tableView.reloadData()
})
}
}
I eventually figured out a solution so I'm posting it here to hopefully help out somebody else. Going off of the example above, replace presentationMode & onChange with this code. Note that if you are using another object in your PushRow besides String, then the type in PushSelectorCell should be that type instead.
$0.presentationMode = PresentationMode.presentModally(
controllerProvider: ControllerProvider.callback {
return SelectorViewController<SelectorRow<PushSelectorCell<String>>> { _ in }
},
onDismiss: { vc in
vc.dismiss(animated: true)
})
$0.onChange({ (row) in
row.reload()
})