How to have a subject in RxSwift push values to itself without creating an infinite loop - swift

I have a UITableView, which I want to put into an editing state if certain conditions are met. The primary way to toggling edit is through an edit button.
So the view elements I have are
let tableView = UITableView()
let editButton = UIButton()
And whether the tableView should be in editing mode is fed from:
let editing = BehaviorSubject(value: false)
Which will be hooked up to the tableView using something like:
editing.subscribeNext { isEditing in
tableView.setEditing(isEditing, animated: true)
}
When the edit button is tapped, I want that to push a new value to editing, that is the negation of the most recent value sent to editing. The most recently value may have been set by a tap on editButton, or it may have come from somewhere else.
I don't understand how to combine the stream for the button press with the stream for editing in such a way that allows this without an infinite loop e.g.
Obervable.combineLatest(editButton.rx_tap.asObservable(), editing) { _, isEditing in
editing.onNext(!isEditing)
}
I'm aware that the tableView has an editing property, but I don't want to rely on that as I am looking for a more general solution that I can re-use elsewhere. I'm also not looking to track the value of isEditing in an instance var, or even as a Variable(), as I am looking for a stateless, stream based solution (if this is at all possible).
Thank you!

With some help from the RxSwift GitHub issues forum I've now worked it out :). The key was withLatestFrom. I've included an example of this below in case it will help anyone else. editButton is the primary way to trigger editing mode on or off, and I've included an event sent via tableView.rx_itemSelected as an additional input example (in this case, I want editing to end any time an item is selected).
let isEditing = BehaviorSubject(value: false)
let tableView = UITableView()
let editButton = UIButton()
tableView.rx_itemSelected
.map { _ in false }
.bindTo(isEditing)
editButton.rx_tap.withLatestFrom(isEditing)
.map { !$0 }
.bindTo(isEditing)
isEditing.subscribeNext { editing in
tableView.setEditing(editing, animated: true)
}
Note: This solution sends .Next(false) to isEditing every time an item is selected, even if the table isn't currently in editing mode. If you feel this is a bad thing, and want to filter rx_itemSelected to only send .Next(false) if the table is actually in editing mode, you could probably do this using a combination of withLatestFrom and filter.

What if you define editing as a Variable instead of a BehaviourSubject. A Variable cannot error out which makes sense in this case. The declaration would look like this:
let editing = Variable(value: false)
You could subscribe to a button tap and change the value of editing to the negated current one:
editButton.rx_tap.asObservable().subscribeNext { editing.value = !editing.value }
With changing the value property of editing this method is called
editing.subscribeNext { isEditing in
tableView.setEditing(isEditing, animated: true)
}
All of this is not tested, but might lead you in the right direction for the right solution.

Related

Mutually exclusive radio buttons for MacOS in Swift

I've got five radio buttons, and selecting one should deselect the others.
I've been over a lot of the questions here about radio buttons in Swift, but they're either for iOS or outdated versions of Swift, because Xcode isn't offering me options like ".isSelected". I've got ".isEnabled" but clearly semantics matter here, because "enabled" isn't the same thing as "selected" and it shows.
Writing my code as a series of "if-else" statements along these lines:
func disableUnselectedButtons() {
if Button2.isEnabled == true {
Button1.isEnabled = false
Button3.isEnabled = false
Button4.isEnabled = false
Button5.isEnabled = false
}
}
results in a situation where I can select all five buttons, and can't DEselect any of them after another has been selected. I've tried variations of .on/.off as well, and can't find the right one for this situation.
It's also clumsy as heck to write a method with five if-else statements along those lines. So there's that.
What's the best way to go about implementing this?
If your radio buttons have the same superview and have the same action then they should work as expected.
To set the same action for each of your radio buttons you can do one of the following.
If you are using Storyboards, open both storyboard and related NSViewController swift file. Ctrl-drag your first radio button to the swift file. Then do the same for each of the other radio buttons ensuring you are dragging onto the function generated from the first Ctrl-drag.
If you are creating the radio buttons in code then set the action parameter in the init for each radio button to be the same.
Another way to approach this is to represent the buttons as a Set and then it's easy to iterate through them and configure their state. The below code actually allows for allowing multiple selections to support a scenario that wants to "select three of the six options".
let allButtons = Set(1...5). //or however many you want
func selectActiveButtons(_ activeButtons: Set<Int>, from allButtons: Set<Int>){
let inactive = allButtons.subtracting(activeButtons)
setButtonState(forButtons: inactive, isSelected: false)
setButtonState(forButtons: activeButtons, isSelected: true)
}
func setButtonState(forButtons buttons: Set<Int>, isSelected: Bool) {
buttons.forEach{
//remove below line and replace with code to update buttons in UI
print("Button \($0): \(isSelected ? "Selected" : "Unselected")")
}
}
// select buttons 1 & 3.
//If wanting a "classic" radio button group just use one value in the arrayLiteral.
selectActiveButtons(Set(arrayLiteral: 1,3), from: allButtons)

Changing the text of a UITextField does not trigger the rx.text binder

I have a UITextField called commentField and I create an Observable<Bool> like this:
let isCommentFieldValid = self.commentField.rx.text.orEmpty.map({ !$0.isEmpty })
This observable determines whether a button is enabled or not.
The problem is that when I change the text property of commentField liked this:
self.commentField.text = ""
The isCommentFieldValid doesn't trigger again and, thus, the button's state doesn't change. Any edition using the UI works: if I remove all text from the field through the keyboard, the isCommentFieldValid updates, but via code it doesn't.
Is there a reason this doesn't work?
If you look at the underlying implementation for rx.text you'll see that it relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Explicitly setting the text property on UITextField does not send actions for these events, so your observable is not updated. You could try sending an action explicitly:
self.commentField.text = ""
self.commentField.sendActions(for: .valueChanged)

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()
})

Show and hide line chart highlight on touch

I want to only highlight a data point when the finger is on the chart, as soon as it lifts off the screen I want to call, or simple deselect the highlight.
func chartValueNothingSelected(chartView: ChartViewBase) {
print("Nothing Selected")
markerView.hidden = true
}
I've tried to override the touch ended but haven't gotten it to work.
You can turn off highlighting any bars/data all together using the highlightEnabled property.
Example of this is:
barChartView.data?.highlightEnabled = false
If you still want to be able to highlight values, but want them to automatically deselect after the touch has ended, I also found another function highlightValues(highs: [ChartHighlight]?) which says in the documentation..
Provide null or an empty array to undo all highlighting.
Call this when you want to deselect all the values and I believe this will work. Example of this could be:
let emptyVals = [ChartHighlight]()
barChartView.highlightValues(emptyVals)
Ref:
Charts Docs: highlightValues documentation
If you don't have to do anything with the tapped data you can use:
barChartView.data?.highlightEnabled = false
If you want to use the tapped data point without displaying the highlight lines, you can use the selection delegate (don't forget to add ChartViewDelegate to your class):
yourChartView.delegate = self // setup the delegate
Add delegate function:
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
// do something with the selected data entry here
yourChartView.highlightValue(nil) // deselect selected data point
}

How to setup printing in cocoa, swift?

I have made printing functionality for custom NSView of NSPopover by the assigning the following action to button for this NSView in mainController:
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPrintfMap(sender: AnyObject)
{
plasmidMapIBOutlet.print(sender)
}
It is working, but the print window has no option for Paper Size and Orientation, see screenshot below.
What should I do to get these options in the print window?
And, how to make the NSView fitting to the printable area? Now it is not fitting.
I have figured out some moments, but not completely. So, I can setup the printing by the following code
#IBAction func actionPrintMap(sender: AnyObject)
{
let printInfo = NSPrintInfo.sharedPrintInfo()
let operation: NSPrintOperation = NSPrintOperation(view: plasmidMapIBOutlet, printInfo: printInfo)
operation.printPanel.options = NSPrintPanelOptions.ShowsPaperSize
operation.printPanel.options = NSPrintPanelOptions.ShowsOrientation
operation.runOperation()
//plasmidMapIBOutlet.print(sender)
}
But, I still have problem. From the code above I can get only orientation (the last, ShowsOrientation), but not both PaperSize and Orientation. How can I manage both ShowsPaperSize and ShowsOrientation?
Finally I have found the answer which is simple to write but it is not really obvious from apple documentation.
operation.printPanel.options.insert(NSPrintPanelOptions.showsPaperSize)
operation.printPanel.options.insert(NSPrintPanelOptions.showsOrientation)
The problem in the code originally posted is that options is being assigned twice, so the first value assigned, ShowsPaperSize is overwritten by the value ShowsOrientation. That's why you only see the ShowsOrientation option in the dialog.
By using multiple insert operations, you are adding options rather than overwriting each time. You can also do it this way which I think reads better:
operation.printPanel.options.insert([.showsPaperSize, .showsOrientation])
And finally, it also works to "set" the options, and by supplying the existing options as the first array value, you achieve the affect of appending:
operation.printPanel.options = [
operation.printPanel.options,
.showsPaperSize,
.showsOrientation
]
(The first array element operation.printPanel.options means that the old options are supplied in the list of new options.)