1

Have an issue with a Driver on RxSwift. Have a view model who is listening to an initTrigger in a ViewController as follow.

    let initTrigger = rx.viewWillAppear
                .mapToVoid()
                .asDriverOnErrorJustComplete()

This initTrigger is used to bind to another Driver on the view model


    let shoppingCart: Driver<ShoppingCart>

    let shoppingCart = input.initTrigger
                .flatMapLatest {
                    self.getShoppingCartUseCase
                        .execute()
                        .asDriver(onErrorJustReturn: ShoppingCart())
                }

getShoppingCartUseCase.execute() returns Observable<ShoppingCart> and is using RxRealm lo listen to changes to a database.

back on the view controller, I have subscribed to that shoppingCart like this

        output?.shoppingCart
            .map {
                print("Mapping")
                return $0.lines.count == 0
            }
            .asObservable()
            .bind(to: goToCartButton.rx.isHidden)
            .disposed(by: bag)

I placed the print("Mapping") to realize that this last Driver is being triggered constantly after making an action that modifies my model and triggers the Observable<ShoppingCart> I mentioned before.

What I'm doing wrong here?

Thanks for your help.

ahsumra
  • 87
  • 10
WedgeSparda
  • 1,161
  • 1
  • 15
  • 40

2 Answers2

2

First of all you can use .distincUntilChanged() to filter identical events. second of all, check why .getShoppingCartUseCase keeps on emitting events, RxRealm will send updates whenever ShoppingCart is written to the db, so maybe you have some unnesessary writes. make sure when you write to realm you use .modified flag, not .all (which will override an item only if it has changed, and won't cause event if it hasn't)

If you sure you only need to an event once - you can always add .take(1) Also you call it initTrigger, but send it on viewWillAppear - which can be called as many times as you getting back to the screen. If you need it once, put it on viewDidLoad

PS instead of .asObservable().bind(to:...) you can just write .drive(...) which is cleaner way to bind drivers to ui.

mas'an
  • 170
  • 6
  • Yes, the issue was due not using `take(1)`. After adding it on my realm query that is being using before updating the model, everything works as expected. Thank you so much. Also, thanks about the advice about using `.drive(...)`. – WedgeSparda Jul 08 '19 at 07:08
  • Glad I helped, but be aware, just adding take(1) is probably symptom fix, not actual bug fix. Try to realise, why your realm observable emit events, maybe there are unnecessary writers to db – mas'an Jul 08 '19 at 16:04
  • I found the reason. It was the method using the Observable that was using the signal to do some changes and save the same model again, triggering the Observable again and creating and infinite loop. In this case the take(1) is the actual bug fix. – WedgeSparda Jul 08 '19 at 19:49
0

To stop subscription observer have to do one of the following:

  1. Send error message

  2. Send completed message

  3. Dispose subscription (destroy disposeBag)

In your case nor rx.viewWillAppear neither shoppingCart not sending error or completed messages, cause they are Drivers

One way for you to stop subscription correctly is to destroy old disposeBag bag = DisposeBag()

But don't forget to restore subscription on viewWillAppear

Other option would be to have some flag in VC like

var hasAppeared: Bool

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    ...
    hasAppeared = true
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisppear(animated)
    ...
    hasAppeared = false
}

and just to add filtering

    output?.shoppingCart
        .filter({ [weak self] _ in self?.hasAppeared ?? false })
        .map {
            print("Mapping")
            return $0.lines.count == 0
        }
        .asObservable()
        .bind(to: goToCartButton.rx.isHidden)
        .disposed(by: bag)

The third way is to stop sending from inside viewModel

let initTrigger = rx.viewWillAppear
            .mapToVoid()
            .asDriverOnErrorJustComplete()
let stopTrigger = rx.viewWillDisappear
            .mapToVoid()
            .asDriverOnErrorJustComplete()


let shoppingCart: Driver<ShoppingCart>

let shoppingCart = Observable.merge(input.initTrigger.map({ true }), 
                                    input.stopTrigger.map({ false }))
            .flatMapLatest { isRunning in
                guard isRunning else {
                    return .just(ShoppingCart())
                }

                return self.getShoppingCartUseCase
                    .execute()
                    .asDriver(onErrorJustReturn: ShoppingCart())
            }
Tiran Ut
  • 942
  • 6
  • 9
  • I see, but I have a problem with this approach, I'm trying to follow the RxSwift MVVM pattern so my viewModel has an Input, with `rx.viewWillAppear` and an output `shoppingCart`, and I have a transform method to connect them. I call this method on viewDidLoad, so I can't use the `hasAppeared` approach. There is another way to destroy the old disposeBag? – WedgeSparda Jul 08 '19 at 05:58
  • hasAppeared is the second option. The first one is to destroy dispose bag with `bag = DisposeBag()` on `viewWillDisappear` and then resubscribe on `viewWillAppear` – Tiran Ut Jul 08 '19 at 06:04