In my app, I have a table view that has a bunch of rows. The table view is populated by an observable. I am doing this using RxDataSources using a RxTableViewSectionedAnimatedDataSource
.
The rows of the table view represent some kind of items that the user can perform actions on. This means that I want to some buttons to appear when a row is selected, and I want those buttons to disappear when no rows are selected. Those buttons are the actions that the user can perform on the items.
I figured that I can observe the indexPathForSelectedRow
property and bind that to button.rx.isHidden
, like so:
[actionButton1, actionButton2, actionButton3].forEach { (button) in
button?.isHidden = true // they are always hidden initially
self.tableView.rx.observe(IndexPath?.self, "indexPathForSelectedRow")
.map { ($0 as? IndexPath) == nil } // $0 is a double optional, so I unwrap it like this
.bind(to: button!.rx.isHidden)
.disposed(by: disposeBag)
}
However, when I select an item, the action buttons do not appear at all.
I also tried to observe indexPathsForSelectedRows
, but it yields the same result.
Then I tried to subscribe to itemSelected
and itemDeselected
individually:
[actionButton1, actionButton2, actionButton3].forEach { (button) in
button?.isHidden = true
self.tableView.rx.itemSelected.subscribe(onNext: {
_ in
button?.isHidden = false
}).disposed(by: disposeBag)
self.tableView.rx.itemDeselected.subscribe(onNext: {
[weak self] _ in
button?.isHidden = (self?.tableView.indexPathForSelectedRow ?? nil) == nil
}).disposed(by: disposeBag)
}
This time, when I select a row, the buttons appear properly.
However, when the observable data source changes, such that the selected row is removed, causing the table view to not have any selected rows anymore, the action buttons do not disappear.
How do I make it so that my action buttons will disappear when the table view does not have any selected rows?
Note that I don't care which row is selected. I just want to know whether any row is selected. In other words, an Observable<Bool>
that emits a new value whenever the statement "the table view has at least one selected row" changes from true to false, or from false to true.
If you are using just RxCocoa should do it:
let itemIsSelected = Observable.merge(
tableView.rx.itemSelected.map { _ in true },
tableViewItems.map { _ in false } // this is the magic you are missing.
)
.startWith(false)
// a much easier way to handle your buttons' hidden state.
for buttonIsHidden in actionButtons.map({ $0.rx.isHidden }) {
itemIsSelected
.map { !$0 }
.bind(to: buttonIsHidden)
.disposed(by: disposeBag)
}
If you are using RxDataSources, the solution is more elaborate:
let itemSelected = tableView.rx.modelSelected(String.self) // or whatever the type is.
let itemIsSelected = Observable.merge(
itemSelected.map { _ in true },
tableViewItems.filter(if: itemSelected) { !$0.contains($1) }
.map { _ in false }
)
.startWith(false)
for buttonIsHidden in actionButtons.map({ $0.rx.isHidden }) {
itemIsSelected
.map { !$0 }
.bind(to: buttonIsHidden)
.disposed(by: disposeBag)
}
The above uses my gist: https://gist.github.com/danielt1263/1a70c4f7b8960d06bd7f1bfa81802cc3
Which contains this function:
extension ObservableType {
/// Filters the source observable sequence using a trigger observable sequence.
/// Elements only go through the filter when the trigger has not completed and
/// its last element produces a true value from the pred. If either source or trigger error's, then the source errors.
///
/// - Parameters:
/// - trigger: The sequence to compare with.
/// - pred: The predicate function to determine if the element should pass through.
/// - Returns: An Observable of the same type that passed the filter test.
func filter<O>(if trigger: O, _ pred: @escaping (Element, O.Element) -> Bool) -> Observable<Element> where O: ObservableType {
return self.withLatestFrom(trigger) { ($0, $1) }
.filter { pred($0.0, $0.1) }
.map { $0.0 }
}
}