Best practice for binding controls in UITableViewCell to ViewModel using RxSwift - ios

I'm in the process of migrating an existing app using MVC that makes heavy use of the delegation pattern to MVVM using RxSwift and RxCocoa for data binding.
In general each View Controller owns an instance of a dedicated View Model object. Let's call the View Model MainViewModel for discussion purposes. When I need a View Model that drives a UITableView, I generally create a CellViewModel as a struct and then create an observable sequence that is converted to a driver that I can use to drive the table view.
Now, let's say that the UITableViewCell contains a button that I would like to bind to the MainViewModel so I can then cause something to occur in my interactor layer (e.g. trigger a network request). I'm not sure what is the best pattern to use in this situation.
Here is a simplified example of what I've started out with (see 2 specific question below code example):
Main View Model:
class MainViewModel {
private let buttonClickSubject = PublishSubject<String>() //Used to detect when a cell button was clicked.
var buttonClicked: AnyObserver<String> {
return buttonClickSubject.asObserver()
}
let dataDriver: Driver<[CellViewModel]>
let disposeBag = DisposeBag()
init(interactor: Interactor) {
//Prepare the data that will drive the table view:
dataDriver = interactor.data
.map { data in
return data.map { MyCellViewModel(model: $0, parent: self) }
}
.asDriver(onErrorJustReturn: [])
//Forward button clicks to the interactor:
buttonClickSubject
.bind(to: interactor.doSomethingForId)
.disposed(by: disposeBag)
}
}
Cell View Model:
struct CellViewModel {
let id: String
// Various fields to populate cell
weak var parent: MainViewModel?
init(model: Model, parent: MainViewModel) {
self.id = model.id
//map the model object to CellViewModel
self.parent = parent
}
}
View Controller:
class MyViewController: UIViewController {
let viewModel: MainViewModel
//Many things omitted for brevity
func bindViewModel() {
viewModel.dataDriver.drive(tableView.rx.items) { tableView, index, element in
let cell = tableView.dequeueReusableCell(...) as! TableViewCell
cell.bindViewModel(viewModel: element)
return cell
}
.disposed(by: disposeBag)
}
}
Cell:
class TableViewCell: UITableViewCell {
func bindViewModel(viewModel: MyCellViewModel) {
button.rx.tap
.map { viewModel.id } //emit the cell's viewModel id when the button is clicked for identification purposes.
.bind(to: viewModel.parent?.buttonClicked) //problem binding because of optional.
.disposed(by: cellDisposeBag)
}
}
Questions:
Is there a better way of doing what I want to achieve using these technologies?
I declared the reference to parent in CellViewModel as weak to avoid a retain cycle between the Cell VM and Main VM. However, this causes a problem when setting up the binding because of the optional value (see line .bind(to: viewModel.parent?.buttonClicked) in TableViewCell implemenation above.

The solution here is to move the Subject out of the ViewModel and into the ViewController. If you find yourself using a Subject or dispose bag inside your view model, you are probably doing something wrong. There are exceptions, but they are pretty rare. You certainly shouldn't be doing it as a habit.
class MyViewController: UIViewController {
var tableView: UITableView!
var viewModel: MainViewModel!
private let disposeBag = DisposeBag()
func bindViewModel() {
let buttonClicked = PublishSubject<String>()
let input = MainViewModel.Input(buttonClicked: buttonClicked)
let output = viewModel.connect(input)
output.dataDriver.drive(tableView.rx.items) { tableView, index, element in
var cell: TableViewCell! // create and assign
cell.bindViewModel(viewModel: element, buttonClicked: buttonClicked.asObserver())
return cell
}
.disposed(by: disposeBag)
}
}
class TableViewCell: UITableViewCell {
var button: UIButton!
private var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
func bindViewModel<O>(viewModel: CellViewModel, buttonClicked: O) where O: ObserverType, O.Element == String {
button.rx.tap
.map { viewModel.id } //emit the cell's viewModel id when the button is clicked for identification purposes.
.bind(to: buttonClicked) //problem binding because of optional.
.disposed(by: disposeBag)
}
}
class MainViewModel {
struct Input {
let buttonClicked: Observable<String>
}
struct Output {
let dataDriver: Driver<[CellViewModel]>
}
private let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
func connect(_ input: Input) -> Output {
//Prepare the data that will drive the table view:
let dataDriver = interactor.data
.map { data in
return data.map { CellViewModel(model: $0) }
}
.asDriver(onErrorJustReturn: [])
//Forward button clicks to the interactor:
_ = input.buttonClicked
.bind(to: interactor.doSomethingForId)
// don't need to put in dispose bag because the button will emit a `completed` event when done.
return Output(dataDriver: dataDriver)
}
}
struct CellViewModel {
let id: String
// Various fields to populate cell
init(model: Model) {
self.id = model.id
}
}

you can use this RxReusable.
this is Rx extension of UITableViewCell, UICollectionView…

Related

RxSwift Input Output, private subject but being triggered outside the class

Im reading this blog about the input output ViewModel approach:
https://medium.com/blablacar-tech/rxswift-mvvm-66827b8b3f10
full code: https://gist.github.com/MartinMoizard
Im just puzzled on how did the
let greeting = validateSubject
.withLatestFrom(nameSubject)
.map { name in
return "Hello \(name)!"
}
.asDriver(onErrorJustReturn: ":-(")
https://gist.github.com/MartinMoizard/4d66528a9959cbbdefa6d50394d2bfb1
is being triggered if the validateSubject is private upon tapped in
https://gist.github.com/MartinMoizard/449be0d30920010210988f1773a2ca90
final class ButtonCell: UITableViewCell, SayHelloViewModelBindable {
#IBOutlet weak var validateButton: UIButton!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
validateButton.rx
.tap
.bind(to: viewModel.input.validate)
.disposed(by: bag)
disposeBag = bag
}
}
TIA
He is not really accessing validateSubject upon tap but eventually he has created some accessible input/output layer see for example:
struct Input {
let name: AnyObserver<String>
let validate: AnyObserver<Void>
}
struct Output {
let greeting: Driver<String>
}
to communicate with the inner private layer/logic (such as validateSubject and nameSubject).
And then he pass all kind of information in input through name and validate and expect information back from the output through greeting.

How edit/delete UICollectionView cells using MVVM and RxSwift

I am trying to understand how to implement MVVM with a list of objects and an UICollectionView. I am not understanding how to implement the User iteration -> Model flow.
I have setup a test application, the Model is just a class with an Int, and the View is an UICollectionViewCell which shows a text with the corresponding Int value and have plus, minus and delete buttons to increment, decrease and remove a element respectively.
Each entry looks like:
I would like to know the best way to use MVVM and RxSwift the update/remove a cell.
I have a list random generated Int values
let items: [Model]
the Model which just have the Int value
class Model {
var number: Int
init(_ n: Int = 0) {
self.number = n
}
}
The ViewModel class which just hold the Model and has an Observable
class ViewModel {
var value: Observable<Model>
init(_ model: Model) {
self.value = Observable.just(model)
}
}
And the Cell
class Cell : UICollectionViewCell {
class var identifier: String { return "\(self)" }
var bag = DisposeBag()
let label: UILabel
let plus: UIButton
let minus: UIButton
let delete: UIButton
....
var viewModel: ViewModel? = nil {
didSet {
....
viewModel.value
.map({ "number is \($0.number)" })
.asDriver(onErrorJustReturn: "")
.drive(self.label.rx.text)
.disposed(by: self.bag)
....
}
}
}
What I don't understand clearly how to do is how to connect the buttons to the corresponding action, update the model and the view afterwards.
Is the Cell's ViewModel responsible for this? Should it be the one receiving the tap event, updating the Model and then the view?
In the remove case, the cell's delete button needs to remove the current Model from the data list. How can this be done without mixing everything all together?
Here is the project with the updates below in GitHub: https://github.com/dtartaglia/RxCollectionViewTester
The first thing we do is to outline all our inputs and outputs. The outputs should be members of the view model struct and the inputs should be members of an input struct.
In this case, we have three inputs from the cell:
struct CellInput {
let plus: Observable<Void>
let minus: Observable<Void>
let delete: Observable<Void>
}
One output for the cell itself (the label) and two outputs for the cell's parent (presumably the view controller's view model.)
struct CellViewModel {
let label: Observable<String>
let value: Observable<Int>
let delete: Observable<Void>
}
Also we need to setup the cell to accept a factory function so it can create a view model instance. The cell also needs to be able to reset itself:
class Cell : UICollectionViewCell {
var bag = DisposeBag()
var label: UILabel!
var plus: UIButton!
var minus: UIButton!
var delete: UIButton!
// code to configure UIProperties omitted.
override func prepareForReuse() {
super.prepareForReuse()
bag = DisposeBag() // this resets the cell's bindings
}
func configure(with factory: #escaping (CellInput) -> CellViewModel) {
// create the input object
let input = CellInput(
plus: plus.rx.tap.asObservable(),
minus: minus.rx.tap.asObservable(),
delete: delete.rx.tap.asObservable()
)
// create the view model from the factory
let viewModel = factory(input)
// bind the view model's label property to the label
viewModel.label
.bind(to: label.rx.text)
.disposed(by: bag)
}
}
Now we need to build the view model's init method. This is where all the real work happens.
extension CellViewModel {
init(_ input: CellInput, initialValue: Int) {
let add = input.plus.map { 1 } // plus adds one to the value
let subtract = input.minus.map { -1 } // minus subtracts one
value = Observable.merge(add, subtract)
.scan(initialValue, accumulator: +) // the logic is here
label = value
.startWith(initialValue)
.map { "number is \($0)" } // create the string from the value
delete = input.delete // delete is just a passthrough in this case
}
}
You will notice that the view model's init method needs more than what is provided by the factory function. The extra info will be provided by the view controller when it creates the factory.
The view controller will have this in its viewDidLoad:
viewModel.counters
.bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { index, element, cell in
cell.configure(with: { input in
let vm = CellViewModel(input, initialValue: element.value)
// Remember the value property tracks the current value of the counter
vm.value
.map { (id: element.id, value: $0) } // tell the main view model which counter's value this is
.bind(to: values)
.disposed(by: cell.bag)
vm.delete
.map { element.id } // tell the main view model which counter should be deleted
.bind(to: deletes)
.disposed(by: cell.bag)
return vm // hand the cell view model to the cell
})
}
.disposed(by: bag)
For the above example I assume that:
counters is of type Observable<[(id: UUID, value: Int)]> and comes from the view controller's view model.
values is of type PublishSubject<(id: UUID, value: Int)> and is input into the view controller's view model.
deletes is of type PublishSubject<UUID> and is input into the view controller's view model.
The construction of the view controller's view model follows the same pattern as the one for the cell:
Inputs:
struct Input {
let value: Observable<(id: UUID, value: Int)>
let add: Observable<Void>
let delete: Observable<UUID>
}
Outputs:
struct ViewModel {
let counters: Observable<[(id: UUID, value: Int)]>
}
Logic:
extension ViewModel {
private enum Action {
case add
case value(id: UUID, value: Int)
case delete(id: UUID)
}
init(_ input: Input, initialValues: [(id: UUID, value: Int)]) {
let addAction = input.add.map { Action.add }
let valueAction = input.value.map(Action.value)
let deleteAction = input.delete.map(Action.delete)
counters = Observable.merge(addAction, valueAction, deleteAction)
.scan(into: initialValues) { model, new in
switch new {
case .add:
model.append((id: UUID(), value: 0))
case .value(let id, let value):
if let index = model.index(where: { $0.id == id }) {
model[index].value = value
}
case .delete(let id):
if let index = model.index(where: { $0.id == id }) {
model.remove(at: index)
}
}
}
}
}
I'm doing it this way:
ViewModel.swift
import Foundation
import RxSwift
import RxCocoa
typealias Model = (String, Int)
class ViewModel {
let disposeBag = DisposeBag()
let items = BehaviorRelay<[Model]>(value: [])
let add = PublishSubject<Model>()
let remove = PublishSubject<Model>()
let addRandom = PublishSubject<()>()
init() {
addRandom
.map { _ in (UUID().uuidString, Int.random(in: 0 ..< 10)) }
.bind(to: add)
.disposed(by: disposeBag)
add.map { newItem in self.items.value + [newItem] }
.bind(to: items)
.disposed(by: disposeBag)
remove.map { removedItem in
self.items.value.filter { (name, _) -> Bool in
name != removedItem.0
}
}
.bind(to: items)
.disposed(by: disposeBag)
}
}
Cell.swift
import Foundation
import Material
import RxSwift
import SnapKit
class Cell: Material.TableViewCell {
var disposeBag: DisposeBag?
let nameLabel = UILabel(frame: .zero)
let valueLabel = UILabel(frame: .zero)
let removeButton = FlatButton(title: "REMOVE")
var model: Model? = nil {
didSet {
guard let (name, value) = model else {
nameLabel.text = ""
valueLabel.text = ""
return
}
nameLabel.text = name
valueLabel.text = "\(value)"
}
}
override func prepare() {
super.prepare()
let textWrapper = UIStackView()
textWrapper.axis = .vertical
textWrapper.distribution = .fill
textWrapper.alignment = .fill
textWrapper.spacing = 8
nameLabel.font = UIFont.boldSystemFont(ofSize: 24)
textWrapper.addArrangedSubview(nameLabel)
textWrapper.addArrangedSubview(valueLabel)
let wrapper = UIStackView()
wrapper.axis = .horizontal
wrapper.distribution = .fill
wrapper.alignment = .fill
wrapper.spacing = 8
addSubview(wrapper)
wrapper.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(8)
}
wrapper.addArrangedSubview(textWrapper)
wrapper.addArrangedSubview(removeButton)
}
}
ViewController.swift
import UIKit
import Material
import RxSwift
import SnapKit
class ViewController: Material.ViewController {
let disposeBag = DisposeBag()
let vm = ViewModel()
let tableView = UITableView()
let addButton = FABButton(image: Icon.cm.add, tintColor: .white)
override func prepare() {
super.prepare()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
addButton.pulseColor = .white
addButton.backgroundColor = Color.red.base
view.layout(addButton)
.width(48)
.height(48)
.bottomRight(bottom: 16, right: 16)
addButton.rx.tap
.bind(to: vm.addRandom)
.disposed(by: disposeBag)
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
vm.items
.bind(to: tableView.rx.items) { (tableView, row, model) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.model = model
cell.disposeBag = DisposeBag()
cell.removeButton.rx.tap
.map { _ in model }
.bind(to: self.vm.remove)
.disposed(by: cell.disposeBag!)
return cell
}
.disposed(by: disposeBag)
}
}
Note that a common mistake is creating the DisposeBag inside Cell only once, this will causing confusing when the action got triggered.
The DisposeBag must be re-created every time the Cell got reused.
A complete working example can be found here.

How do you communicate between a UIViewController and its child UIView using MVVM and RxSwift events?

I'm using MVVM, Clean Architecture and RxSwift in my project. There is a view controller that has a child UIView that is created from a separate .xib file on the fly (since it is used in multiple scenes). Thus there are two viewmodels, the UIViewController's view model and the UIView's. Now, there is an Rx event in the child viewmodel that should be observed by the parent and then it will call some of its and its viewmodel's functions. The code is like this:
MyPlayerViewModel:
class MyPlayerViewModel {
var eventShowUp: PublishSubject<Void> = PublishSubject<Void>()
var rxEventShowUp: Observable<Void> {
return eventShowUp
}
}
MyPlayerView:
class MyPlayerView: UIView {
var viewModel: MyPlayerViewModel?
setup(viewModel: MyPlayerViewModel) {
self.viewModel = viewModel
}
}
MyPlayerSceneViewController:
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
#IBOutlet weak var otherView: UIView!
var viewModel: MyPlayerSceneViewModel
fileprivate var disposeBag : DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.myPlayerView.viewModel.rxEventShowUp.subscribe(onNext: { [weak self] in
self?.viewModel.doOnShowUp()
self?.otherView.isHidden = true
})
}
}
As you can see, currently, I am exposing the myPlayerView's viewModel to the public so the parent can observe the event on it. Is this the right way to do it? If not, is there any other suggestion about the better way? Thanks.
In general, nothing bad to expose view's stuff to its view controller but do you really need two separate view models there? Don't you mix viewModel and model responsibilities?
Some thoughts:
Model shouldn't subclass UIView.
You should avoid creating own subjects in a view model. It doesn't create events by itself, it only processes input and exposes results.
I encourage you to get familiar with Binder and Driver.
Here is the code example:
struct PlayerModel {
let id: Int
let name: String
}
class MyPlayerSceneViewModel {
struct Input {
let eventShowUpTrigger: Observable<Void>
}
struct Output {
let someUIAction: Driver<PlayerModel>
}
func transform(input: Input) -> Output {
let someUIAction = input.eventShowUpTrigger
.flatMapLatest(fetchPlayerDetails) // Transform input
.asDriver(onErrorJustReturn: PlayerModel(id: -1, name: "unknown"))
return Output(someUIAction: someUIAction)
}
private func fetchPlayerDetails() -> Observable<PlayerModel> {
return Observable.just(PlayerModel(id: 1, name: "John"))
}
}
class MyPlayerView: UIView {
var eventShowUp: Observable<Void> {
return Observable.just(()) // Expose some UI trigger
}
var playerBinding: Binder<PlayerModel> {
return Binder(self) { target, player in
target.playerNameLabel.text = player.name
}
}
let playerNameLabel = UILabel()
}
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
private var viewModel: MyPlayerSceneViewModel!
private var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
let input = MyPlayerSceneViewModel.Input(eventShowUpTrigger: myPlayerView.eventShowUp)
let output = viewModel.transform(input: input)
// Drive manually
output
.someUIAction
.map { $0.name }
.drive(myPlayerView.playerNameLabel.rx.text)
.disposed(by: disposeBag)
// or to exposed binder
output
.someUIAction
.drive(myPlayerView.playerBinding)
.disposed(by: disposeBag)
}
}

Binding UITextField or button with viewModel

I have been facing an issue with binding UITextField or button with observables in viewModel.
class VM {
var emailObservable: Observable<String?> = Observable.just("")
}
I have this observable for email in my viewModel and in controller. When i try to bind my textfield with it, it gives me error
Cannot invoke 'bind' with an argument list of type '(to: Observable)'.
But when i replace the observables with Variable, it works fine.
Can someone please help me with this. I found answers which mainly include passing the observable in the init method of viewModel, but i don't want to pass it in the init method.
This is the link i found for binding but it is through init method.
How to bind rx_tap (UIButton) to ViewModel?
Instead of
emailTextfield.rx.text.asObservable().bind(to: viewModel.emailObservable).disposed(by: disposeBag)
use this code
viewModel.emailObservable.bind(to: noteField.rx.text).disposed(by: disposeBag)
Probably, you want to make two way binding, so read more about it here
I think here what you looking for:
final class ViewModel {
private let bag = DisposeBag()
let string = BehaviorSubject<String>(value: "")
init() {
string.asObservable().subscribe(onNext: { string in
print(string)
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
textField.rx.text
.orEmpty
.bind(to: viewModel.string)
.disposed(by: bag)
}
}
Note, as #MaximVolgin mentioned Variable is deprecated in RxSwift 4, so you can use BehaviorSubject or other that's up to you.
UPD.
Implementation with Observable only.
final class ViewModel {
private let bag = DisposeBag()
var string = "" {
didSet {
print(string)
}
}
init(stringObservable: Observable<String>) {
stringObservable.subscribe(onNext: { string in
self.string = string
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel(stringObservable: textField.rx.text.orEmpty.asObservable())
}
}
As you can see, your solution can be implemented using Observable, not Variable or any kind of Subject. Also should be mentioned that in most cases this is not the final logic (just bind textField or whatever to some variable). There can be some validation, enable/disable, etc. logic. For this cases RxSwift provide Driver. Also nice example about differences in using Observable and Driver for one project can be found here (by RxSwift).
Method .bind(to:) binds to an Observer, not Observable.
Variable (deprecated in RxSwift v4) is a special-purpose Subject.
Subjects are by definition both Observer and Observable.
This is what .bind(to:) does inside -
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
UPDATE:
How to avoid passing observables in .init() of VM:
// inside VM:
fileprivate let observableSwitch: BehaviorSubject<Observable<MyValue>>
fileprivate let myValueObservable = observableSwitch.switchLatest()
// instead of passing in init:
public func switch(to observable: Observable<MyValue>) {
self.observableSwitch.onNext(observable)
}
Take a subject of variable type in ViewModel class:
class ViewModel{
//MARK: - local Variables
var emailText = Variable<String?>("")
}
Now create object of viewmodel class in viewController class and bind this emailtext variable to textfield in viewcontroller.Whenever textfield text will change then it emailText of viewmodel gets value.
txtfield.rx.text
.bindTo(viewModel.emailText).addDisposableTo(disposeBag)
Try this,
override func viewDidLoad() {
super.viewDidLoad()
_ = userNameTextField.rx.text.map { $0 ?? "" }.bind(to: viewModel.userName)
}
in viewModel class,
class ViewModel{
var userName: Variable<String> = Variable("")
}

MVVM and RxSwift for Search Screen

In purpose of education MVVM and RxSwift I want to build simple search screen, which will have a table view and a search bar. When user types something into the search bar I will show what he have in this table. Sounds pretty simple, but I can't find any tutorial which suits me.
I have already written all code in view controller, I just can't understand have to observe search text changes and then call database method, which will filter items by search text.
Some code, which I already have.
My ViewController
import Foundation
import UIKit
import RxSwift
import RxCocoa
class PlaceSearchViewController: UIViewController {
//MARK: -
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
//MARK: - Dependencies
private var viewModel: PlaceSearchViewModel!
private let disposeBag = DisposeBag()
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel = PlaceSearchViewModel()
addBindsToViewModel(viewModel)
}
//MARK: - Rx
private func addBindsToViewModel(viewModel: PlaceSearchViewModel) {
searchBar.rx_text.bindTo(viewModel.searchTextObservable)
viewModel.placesObservable.bindTo(tableView.rx_itemsWithCellFactory) {
(tableView: UITableView, index, place: Place) in
let indexPath = NSIndexPath(forItem: index, inSection: 0)
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as PlaceCell
cell.configureWithObject(place)
return cell
}
.addDisposableTo(disposeBag)
tableView.rx_contentOffset
.subscribe { _ in
if self.searchBar.isFirstResponder() {
_ = self.searchBar.resignFirstResponder()
}
}
.addDisposableTo(disposeBag)
}
}
And my view model:
import Foundation
import RxSwift
import RxCocoa
class PlaceSearchViewModel {
//MARK: - Dependecies
private let disposeBag = DisposeBag()
//MARK: - Model
private let placesObservable: Observable<[Place]>
var searchTextObservable = Variable<String>("")
//MARK: - Set up
init() {
placesObservable = searchTextObservable.asObservable()
//wait 0.3 s after the last value to fire a new value
.debounce(0.3, scheduler: MainScheduler.instance)
//only fire if the value is different than the last one
.distinctUntilChanged()
//convert Observable<String> to Observable<[Place]>
.flatMapLatest { searchString -> Observable<[Place]> in
// some code here which I can't write.
}
//make sure all subscribers use the same exact subscription
.shareReplay(1)
}
}
Also, I have method [DataBase searchPlaces:searchText] which returns array of places - [Place]. I can't understand where and how place it in flatMapLatest of my ViewModel.
I create reactive wrapper for my DataBase by creating Observable<[Place]>.
This is my code
placesObservable = searchTextObservable.asObservable()
//wait 0.3 s after the last value to fire a new value
.debounce(0.0, scheduler: MainScheduler.instance)
//only fire if the value is different than the last one
.distinctUntilChanged()
//convert Observable<String> to Observable<Weather>
.flatMapLatest { searchString -> Observable<[AnyObject]> in
return TPReactiveDatabase.sharedInstance.searchPlacesByTitle(searchString)
}
//make sure all subscribers use the same exact subscription
.shareReplay(1)
And wrapper method searchPlacesByTitle
class TPReactiveDatabase: NSObject {
static let sharedInstance = TPReactiveDatabase()
// MARK: - Reactive Place database
func searchPlacesByTitle(title: String) -> Observable<[AnyObject]> {
return Observable.create { observer in
var places = [AnyObject]()
if (title.characters.count > 0) {
places = DBAccessKit.searchPlacesByTitle(title)
}
observer.on(.Next(places))
observer.on(.Completed)
return AnonymousDisposable {
}
}
}
}

Resources