Rizwan Saeed
Rizwan Saeed

Reputation: 153

Viewing initial data source and filtering with RxSwift using MVVM

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

Answers (3)

Rizwan Saeed
Rizwan Saeed

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

Alexandr Kolesnik
Alexandr Kolesnik

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

karem_gohar
karem_gohar

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
    }

}
  • have in your VM the original list
  • create different filtered list as below
var cellViewModels: Observable<[/* your model */]?>
  • have the search bar delegate method in the View as below - remember it will vary depending on your implementation -
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 */
    }

}

  • note that this solution does not use RxSwift it uses only foundation

Upvotes: 0

Related Questions