Reputation: 153
I'm starting out with RxSwift and trying to get a simple example of filtering a data source with a UISearchController working.
I have the basic setup of a UISearchController wired into a UITableViewController. Using MVVM I also have a basic view model setup that will drive the table.
self.viewModel.searchText.accept(searchController.searchBar.text ?? "")
viewModel.listItems.bind(to: tableView.rx.items(cellIdentifier: "ItemCell")) { row, item, cell in
cell.textLabel!.text = item.name
}
.disposed(by: disposeBag)
View Model
class ListViewModel {
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno")
])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> = Observable.just([])
init() {
listItems = sourceItems.asObservable()
}
}
I can add in the search filtering and this works such that only the values matching the filter string will show
let searchObservable = searchText
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { query in
return query.count > 2
}
.share(replay: 1)
listItems = Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}
However, this will not show any values until the filter is matched. What I am trying to do is initially show all the values and then only show the filtered values. I'm not quite sure how to populate the listItems
when the searchText
changes but is empty or events are filtered out.
Upvotes: 1
Views: 1689
Reputation: 153
With thanks to @AlexandrKolesnik I managed to tweak his answer and get it working:
class ListViewModel {
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno"),
ListItem(name: "abcdef")
])
private var filteredItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> {
return filteredItems.asObservable()
}
private let disposeBag = DisposeBag()
init() {
searchText
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe({ [unowned self] event in
guard let query = event.element, !query.isEmpty else {
self.filteredItems.accept(self.sourceItems.value)
return
}
self.filteredItems.accept(
self.sourceItems.value.filter {
$0.name.lowercased().contains(query.lowercased())
}
)
})
.disposed(by: disposeBag)
}
}
Upvotes: 0
Reputation: 2204
You forgot to subscribe for changes, instead of
listItems = Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}
should be
Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
print(resultArray) // here you can change your listItems
})
.disposed(by: disposeBag)
this is how to change searchText searchText.accept("123")
UPDATED:
to handle any searchBar updates you should implement serachBar.rx Here is some example how to
import UIKit
import RxSwift
import RxCocoa
class ListItem: NSObject {
var name: String = ""
public init(name str: String) {
super.init()
name = str
}
}
class ViewController: UIViewController, UISearchBarDelegate {
@IBOutlet weak var searchBar: UISearchBar!
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno")
])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> = Observable.just([])
var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
addSearchBarObserver()
listItems = sourceItems.asObservable()
Observable.combineLatest(sourceItems.asObservable(), searchText) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
print(resultArray)
})
.disposed(by: disposeBag)
}
private func addSearchBarObserver() {
searchBar
.rx
.text
.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [weak self] query in
guard
let query = query.element else { return }
self?.searchText.accept(query)
}
.disposed(by: disposeBag)
}
}
Upvotes: 1
Reputation: 336
my approach will be the following
create such observable class as below
import Foundation
class Observable<Generic> {
var value: Generic {
didSet {
DispatchQueue.main.async {
self.valueChanged?(self.value)
}
}
}
private var valueChanged: ((Generic) -> Void)?
init(_ value: Generic) {
self.value = value
}
/// Add closure as an observer and trigger the closure imeediately if fireNow = true
func addObserver(fireNow: Bool = true, _ onChange: ((Generic) -> Void)?) {
valueChanged = onChange
if fireNow {
onChange?(value)
}
}
func removeObserver() {
valueChanged = nil
}
}
var cellViewModels: Observable<[/* your model */]?>
class TradeDealsViewController: UIViewController, UISearchBarDelegate {
// Mark :- IB Outlets
@IBOutlet weak var collectionView: UICollectionView!
private var viewModel: ViewModel /* your VM */?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel = TradeDealsViewModel()
bindViewLiveData()
}
private func bindViewLiveData(){
viewModel?.cellViewModels.addObserver({ [weak self] (responsee) in
self?.collectionView.reloadData()
})
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
/* do your filtration logic here */
}
}
Upvotes: 0