RxSwift - trigger search programmatically in UISearchBar - ios

I am observing UISearchBar.rx.text attributes to perform some search related Action when User types some text.
But at some time, I also would like to trigger this search Action programmatically. For instance at the creation of the view like in this example, where unfortunately the "Searching for [...]" text is not printed.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var mySearchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Trigger search when text changes
mySearchBar.rx.text.subscribe(onNext: { (text)
print("Searching for \(text)...")
// do some search Action
}
.disposed(by: disposeBag)
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
}
}
The problem is changing mySearchBar.text does not trigger a new rx.text Event. Is there some way to do so?
For instance, I know thanks to this post that with an UITextField, this is possible using the UITextField.sendActions(for: .ValueChanged) function. Is there some similar way to do so with UISearchBar?

You could use a Variable<String?> as a sink for your search bar updates. That way you could also set its value programmatically, and use the variable instead of the search bar directly to drive your action:
class ViewController: UIViewController {
let searchText = Variable<String?>(nil)
let searchBar = UISearchBar()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
searchBar.rx.text.asDriver()
.drive(searchText)
.disposed(by: disposeBag)
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
if let welf = self, welf.searchBar.text != text {
welf.searchBar.text = text
}
})
.disposed(by: disposeBag)
searchText.value = "variables so cool"
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
self?.doStuff(text)
})
.disposed(by: disposeBag)
}
}

This is a simple usage of Drivers
import UIKit
import RxCocoa
import RxSwift
class SearchViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let dataModels = Driver.just(["David", "Behrad","Tony","Carl","Davidov"])
let searchingTextDriver = searchBar.rx.text.orEmpty.asDriver()
let matchedCases = searchingTextDriver.withLatestFrom(dataModels){ searchingItem,models in
return models.filter({ (item) -> Bool in
return item.contains(searchingItem)
})
}
matchedCases.do(onNext: { (items) in
print(items)
}).drive().disposed(by: disposeBag)
}
}
As you see we created a dataModels Driver that it could be a response of your network or dataBase fetching.
the searching text converted to Driver then mixed with the dataModels to create matchedCases.

Yes same works for UITextBar.
Setting .text programmatically does not trigger the action automatically, I don't know why! but this is the case for UITextField too.
Call sendActions after each time you update .text property of a UIView subclass.
Try this :
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
mySearchBar.sendActions(for: .valueChanged)

Related

RxSwift calling bind fires immediately vs subscribe(onNext: )

Everything I've read says that bind(to:) calls subscribe(onNext:) within it. So I assume I should be able to swap out some stuff, but when I use `bind(to:) the thing it's binding to fires immediately. Here's my example:
ButtonCollectionViewCell
class ButtonCollectionViewCell: UICollectionViewCell {
lazy var buttonTapped: Observable<Void> = { _buttonTapped.asObservable() }()
private var _buttonTapped = PublishSubject<Void>()
private var disposeBag = DisposeBag()
#IBOutlet private weak var textLabel: UILabel!
#IBOutlet private weak var actionButton: UIButton!
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
actionButton.rx.tap.bind(to: _buttonTapped).disposed(by: disposeBag)
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}
}
Now when I do the following below everything works as expected and it prints to the console when I tap the button
ViewController with a collection view
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell() // there's more code to create it, this is just for simplicity
buttonCell.buttonTapped.subscribe { _ in
print("tapped")
}.disposed(by: disposeBag)
return buttonCell
}
However, if I change the above to:
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell()
buttonCell.buttonTapped.bind(to: buttonTapped)
return buttonCell
}
private func buttonTapped(_ sender: Observable<Void>) {
print("tapped")
}
The "tapped" is printed out right before I get to the cell when scrolling which I assume is when it's being created.
I don't understand this. I thought I could almost swap out the implementations? I would like to use the second example above there as I think it's neater but can't figure out how.
Your two examples are not identical...
In the first example you have: .subscribe { _ in print("tapped") } which is not a subscribe(onNext:) call. The last closure on the subscribe is being used, not the first. I.E., you are calling subscribe(onDisposed:).
Also, your ButtonCollectionViewCell is setup wrong. You bind in the awakeFromNib() which is only called once, and dispose in the prepareForReuse() which is called multiple times. One of the two needs to be move to a more appropriate place...
UPDATE
You could either rebind your subject after reseating the disposeBag, or you could just not put the chain in the dispose bag in the first place by doing:
_ = actionButton.rx.tap
.takeUntil(rx.deallocating)
.bind(to: _buttonTapped)

How to use RxSwift to implement whether a view is hidden by a button's click state (isSelected)

I want to have the view hidden when the button is selected and the view shown when button is deselected, how do I do it using RxSwift?
When I created a custom control, a checkbox out of UIButton, I actually struggled observing that isSelected property. But here's an easy way:
Subclass the UIButton (this is optional, but in this way, you get shorter lines in your controller).
In your custom button, subscribe to its own .rx.tap.
Have a BehaviorRelay called isSelectedBinder in your button.
Finally, you can now bind that isSelectedBinder of your instantiated button to the .rx.isHidden of your whatever view.
Controller
class ViewController: UIViewController {
#IBOutlet weak var button: MyButton!
#IBOutlet weak var someView: UIView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.button.isSelectedBinder
.bind(to: self.someView.rx.isHidden)
.disposed(by: disposeBag)
}
}
Button
class MyButton: UIButton {
var isSelectedBinder = BehaviorRelay<Bool>(value: true)
let disposeBag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
weak var weakSelf = self
self.rx.tap.subscribe { _ in
print("TAP")
guard let strongSelf = weakSelf else { return }
strongSelf.isSelectedBinder.accept(!strongSelf.isSelectedBinder.value)
}.disposed(by: self.disposeBag)
}
}

RxSwift Observeable doesn't update tableview on data change

I setup the tableview and want to update the UI as soon as the data changes. I simulate a data change with the dispatcher. But the problem is, that the table won't update. Can someone explain how to setup a tableview with RxSwift to update it's cell on data change?
#IBOutlet private var tableView: UITableView!
let europeanChocolates: Variable<[Chocolate]> = Variable([])
let disposeBag = DisposeBag()
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
//MARK: Rx Setup
private func setupCellConfiguration() {
europeanChocolates.asObservable().bindTo(tableView.rx.items(cellIdentifier: ChocolateCell.Identifier, cellType: ChocolateCell.self)) {
row, chocolate, cell in
cell.configureWithChocolate(chocolate: chocolate)
}
.addDisposableTo(disposeBag)
}
You didnt provide implementation of your Chocolate, but I assume it's structure, in that case you are not changing anything, because in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
your chocolate from europeanChocolates is copied to new var choclate, but you never save it. You can easily check it, if you try this
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
europeanChocolates.asObservable().subscribe(onNext: { choco in
print("choco changed \(choco)")
})
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
I think it won't print "choco changed". If Im right this should help you:
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
self.europeanChocolates.value[0] = choclate
}
}
If it doesn't help, please provide implementation of Chocolate and ChocolateCell
In swift5 the Variable has been deprecated. So preferably you should use the behaviour subjects. To pass value in it use OnNext function. If it bind with the table view then it will automatically refresh the new data.
You have to reload the tableView just after the data change. To reload tableView call this:
this.tableView.reloadData()

How to recovery dataSource after disposed from a catchError

I have one UITableView populated by reactive viewmodel using RxSwift, pagination and refresh are working well. The viewModel.dataSource() is consuming my API and sometime I can receive a empty result parsed as error type.
I want to catch this error and create an empty state, hiding tableview and showing a emptyViewState. I thought I could make it with the catchError.
My problem is after catchError, the dataSource is disposed and I couldn't be able to recovery the empty state and repopulated the tableview, I tried to recreate the dataSource calling self.bindDataSource() but I getting fatal error.
There is a way to avoid dataSource disposed ? How can I reconnect / rebuild the dataSource to recovery from the empty state ?
class MyViewControl: UIViewController {
fileprivate let disposeBag = DisposeBag()
fileprivate let viewModel = ViewModel()
let dataSource = SearchViewModel.SearchDataSource()
#IBOutlet fileprivate weak var tableView: UITableView!
#IBOutlet weak var emptyStateView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// When I disable tableview, can see a hidden view with empty state message and one button
viewModel.isTableViewHidden
.bindTo(tableView.rx.isHidden)
.addDisposableTo(disposeBag)
self.setupTableView()
}
fun setupTableView() {
// ... setup table view
self.bindDataSource()
}
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
viewModel.dataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBag )
}
#IBAction fileprivate func emptyStateAction(_ sender: UIButton) {
// Do something and try to recreate the bindDataSource
self.bindDataSource()
}
}
class SearchViewModel {
private let disposeBag = DisposeBag()
typealias SearchDataSource = RxTableViewSectionedReloadDataSource<PaginationStatus<WorkerEntity>>
let isTableViewHidden = BehaviorSubject<Bool>(value: false)
// Controls to refresh and paging tableview
let refreshTrigger = BehaviorSubject<Void>(value:())
let nextPageTrigger = PublishSubject<Void>()
// Others things happing herer
func dataSource() -> Observable<[PaginationStatus<WorkerEntity>]> {
return self.refreshTrigger.debug("[DEBUG] Refreshing dataSource")
.flatMapLatest { [unowned self] _ -> Observable<[PaginationStatus<WorkerEntity>]> in
// Access the API and return dataSource
}
.catchError { [unowned self] error -> Observable<[PaginationStatus<WorkerEntity>]> in
// Hidden the tableview
self.isTableViewHidden.onNext(true)
// Do others things
return Observable.of([PaginationStatus.sectionEmpty])
}
}
}
when you bindDataSource() you dont reinitialised your datasource, so you bind it to a error event.
You need to init it, to bind it again. And you might want to remove your binding too
let disposeBagTableView = DisposeBag()
//remove
let dataSource = SearchViewModel.SearchDataSource()
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
disposeBagTableView = DisposeBag()
SearchViewModel.SearchDataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBagTableView )
}

Unexpectedly found nil while unwrapping an optional value uitextfield

I know there are a lot of unexpected nil value questions here, but I've tried other approaches and they're not working for me. The function at the bottom is called from a button on another view controller. Basically the issue is that this code works as expected, the form element has the DatePicker as the input view, and it updates startDatePickerField.text when the value is changed, so I know that startDatePickerField.text is not empty, as it is displayed on the screen. However, when calling the testForValidity() function is called from another view controller containing the one we are working in, I get the "unexpectedly found nil while unwrapping option value error". All of the outlets are connected properly so I know it isn't that.
class popoverTableViewController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet var startDatePickerField: UITextField!
override func viewDidLoad() {
let startDatePicker:UIDatePicker = UIDatePicker()
startDatePicker.datePickerMode = UIDatePickerMode.dateAndTime
startDatePickerField.inputView = startDatePicker
startDatePicker.minuteInterval = 5
startDatePicker.addTarget(self, action: #selector(popoverTableViewController.startDatePickerValueChanged(_:)), for: UIControlEvents.valueChanged)
}
#IBAction func startDateDidBegin(_ sender: AnyObject) {
}
func startDatePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = DateFormatter.Style.long
dateFormatter.timeStyle = DateFormatter.Style.short
startDatePickerField.text = dateFormatter.string(from: sender.date)
finishDateTime = sender.date
}
And the function that errors:
func testForValidity() {
print(startDatePickerField.text)
}
Issue:
You're trying to access IBOutlet property from another class via instance method.
The reason behind the nil value is IBOutlets are initialized when the nib is loaded.
Hence in your case when you are in another view controller you can't access an Outlet variable else you will be always getting nil.
Solution:
If I assume correctly, you want to use a value from a view controller after having a value in it.
The best way is to follow 'Design patterns'. i.e., You have to store your required value in a model class and access the value via model class instance.
Apple has very good documentation about it. Please go through.
https://developer.apple.com/library/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson6.html#//apple_ref/doc/uid/TP40015214-CH20-SW1
Sample Code in Swift:
class TextFieldViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var outputLabel : UILabel!
#IBOutlet weak var textField : UITextField!
override func viewDidLoad() { super.viewDidLoad(); setupTextField() }
func testValidity() -> (textFieldValue: UITextField?, modelValue: String?) {
return (textField, TextFieldModel.sharedModel.textFieldData)
}
//MARK: - Text Field Delegates
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
outputLabel.text = textField.text
TextFieldModel.sharedModel.textFieldData = textField.text
return true
}
}
class TextFieldModel {
static let sharedModel = TextFieldModel()
var textFieldData: String?
}
class DemoController: UIViewController {
#IBOutlet weak var textFieldOutput: UILabel!
#IBOutlet weak var modelOutput: UILabel!
override func viewDidLoad() { super.viewDidLoad(); testValues() }
func testValues() {
let tvc = TextFieldViewController()
textFieldOutput.text = "Value of Text field is " + (tvc.testValidity().textFieldValue?.text ?? "nil")
modelOutput.text = "Value accessed from Model Class is \(tvc.testValidity().modelValue)"
}
}
extension TextFieldViewController {
func setupTextField() {
textField.placeholder = ""
textField.delegate = self
textField.becomeFirstResponder()
textField.keyboardType = UIKeyboardType.Default
textField.returnKeyType = UIReturnKeyType.Done
}
}
Output:
At the top, add UITextFieldDelegate so it becomes:
class popoverTableViewController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
Then in viewDidLoad() add this line :
self.startDatePickerField.delegate = self

Resources