issue with having rx.tap for UIButton in UICollectionViewCell - RxSwift 3
Asked Answered
T

2

9

I am subscribing 2 times for 1 UIButton :

  1. First subscription, for updating UI on every click
  2. Second subscription, for updating the values on Web Service every 1 second after accumulated clicks.

Code:

class ProductionSize {
    var id : Int?
    var size: Int = 0
    var name: String = ""
}

class ProductionCell: UICollectionViewCell {
    var rxBag = DisposeBag()


    // this will be set in the (cellForItemAt indexPath: IndexPath) of collection view
    var productionSize: ProductionSize? {
        didSet {
            showProductionSize()
            prepareButton()
        }
    }

    func showProductionSize() {
        // ... code for showing ProductionSize in labels
    }

    func prepareButton() {
        // This for subscribing for every click for displaying purpose

        btn_increase.rx.tap
            .subscribe(){event in 
                self.increaseClicked() 
            }
            .addDisposableTo(rxBag)

        // this for subscribing for sending webservice request after 1 second of clicking the button (so that if user click it quickly i send only last request)

        btn_increase.rx.tap
            .debounce(1.0, scheduler: MainScheduler.instance)
            .subscribe(){ event in self.updateOnWS() }
            .addDisposableTo(rxBag)
    }

    func increaseClicked() {
        productionSize.size = productionSize.size + 1
        showProductionSize()
    }

    func updateOnWS() {
        // code for updating on webservice with Moya, RxSwift and Alamofire§
    }


    // when scrolling it gets called to dispose subscribtions
    override func prepareForReuse() {
        rxBag = DisposeBag()
    }

}

The problem:

Since the dispose happens on prepareForReuse(), If i click on the button many times and scroll immediately, the webservice calls gets disposed and not updated.

what I have tried:

  1. Added addDisposableTo(vc?.rx_disposableBag) to the parent ViewController DisposableBag.

    The problem, the subscribtions accumulated and on every click the updateWS() called many times which is subscribed on every scroll and never disposed.

  2. I have tried to remove the disposableBag re-initialization from prepareForReuse().

    The problem, Again the subscriptions to the buttons getting duplicated and accumulated and many webservice calls get called every click.

Question: How can I get the debounce subscriptions called to the end and never repeated with multiple subscriptions (in case of addDisposableTo viewController Bag) ?

Tellurate answered 4/4, 2017 at 12:9 Comment(5)
Not sure about your case so please clarify: Is the productionSize used in your updateOnWS? Such that the user can tap the button many times and then when they stop tapping, the number (i.e. tapped 5 times) 5 is used for the network call? And what should the behavior be when you scroll down and dispose the cell; should the network request be disposed or should it still be performed? And should production size UI be updated on tap of the user, or on successful response of the network request?Schock
Also, be careful with how you use self inside closures, this can cause memory leaks that add up, especially when it is in UITableViewCells or UICollectionViewCells. It prevents the object from being disposed. See #40584185Schock
@iwillnot i update the productionSize every click so that on the webservice call, i will take the last productionSize value and send it in the updateOnWS. if tap 5 times, it will only update 1 time on the network. i dont wait the response, for better user experience i show the ui updates them i uodate it if the request was successfully doneTellurate
What should the behavior be when you scroll down and dispose the cell; should the network request be disposed or should it still be performed?Schock
@iwillnot the request should be performedTellurate
O
11

Since prepareButton() is always called in the (cellForItemAt indexPath: IndexPath) of collection view you could try this:

func prepareButton() {

    self.rxBag = nil 
    let rxBag = DisposeBag()

    // This for subscribing for every click for displaying purpose

    btn_increase.rx.tap
        .subscribe(onNext: { [weak self] _ in 
            self?.increaseClicked()
        })
        .addDisposableTo(rxBag)

    // this for subscribing for sending webservice request after 1 second of clicking the button (so that if user click it quickly i send only last request)

    btn_increase.rx.tap
        .debounce(1.0, scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] _ in 
            self?.updateOnWS()
        })
        .addDisposableTo(rxBag)

    self.rxBag = rxBag
}

Remove the prepareForReuse() implementation.

Oliver answered 11/4, 2017 at 15:33 Comment(2)
thank you for the answer. it looks like your code does exactly same as prepareForReuse(), since the prepareButton gets called everytime the cell reused on scrolling, so that it will dispose the request before it finishes. anyway i will test it and share the result againTellurate
again the request was canceled very early if i click the button and scroll immediately without waiting debounce time (1 sec).Tellurate
S
7

Added addDisposableTo(vc?.rx_disposableBag) to the parent ViewController DisposableBag.

The problem, the subscribtions accumulated and on every click the updateWS() called many times which is subscribed on every scroll and never disposed.

It is possible that your self.updateOnWS() gets called many times because of how you subscribe to the button's tap.

    btn_increase.rx.tap
        .debounce(1.0, scheduler: MainScheduler.instance)
        .subscribe(){ event in self.updateOnWS() }
        .addDisposableTo(rxBag)

As you can see, you subscribe to all events using the subscribe() method. This means that all Rx events (onNext, onError, onCompleted, onSubscribed, and onDisposed) trigger the self.updateOnWS(). You can check if this is the case by printing out the event object to see what event was triggered.

Subscribe on onNext only

A possible fix might be to only subscribe to the onNext operation.

    btn_increase.rx.tap
        .debounce(1.0, scheduler: MainScheduler.instance)
        .subscribe(onNext: { [weak self] (_ : Void) in
               self?.updateOnWS() 
        })
        .addDisposableTo(vc?.rxdisposableBag)

By using the DisposeBag of the view controller, you can make sure that the operation still continues even if the cell gets disposed (when you scroll down). However, if you need it to dispose the subscription when the cell gets disposed, use the DisposeBag of the cell, not the view controller.

Side note - Memory leak

Notice that the reference to self is specified to be weak, so that you can prevent memory leaks from happening. By specifying it to be weak, it will provide you a reference to self that is optional.

Without doing this, the closure you created for the onNext block will retain a strong reference to the self which is your UICollectionViewCell, which in turn owns the very closure we are discussing.

This will eventually cause a out of memory crash. See the reference I posted on the comments of your question for more things to read about memory leaks caused by incorrectly referencing self.

Schock answered 4/4, 2017 at 16:20 Comment(5)
thank you so much for your answer. It was good to know about the memory leak, but there is big problem when i subscribe to .addDisposableTo(vc?.rxdisposableBag) . the old subscriptions are not disposed on scroll it accumulate many subscriptions and many calls implemented to webservice.Tellurate
can we dispose, without canceling the old call ?Tellurate
You can manually handle your subscriptions by calling dispose on the subscription object. Or have a DisposeBag that you can re-instantiate to simulate a dispose.Schock
what can be another solution for this problem generally ?Tellurate
I'd consider using the debounce, throttle operators, and even probably seeing if buffer with filter with condition count > 0 gets the behavior that I want. Anything that could probably reduce the subscription code from running multiple times. For the disposal of old subscriptions, I'd use a separate dispose bag that I clean up manually or conside the ` flatMapLatest operator. If the answer helped, please mark it as accepted. :)Schock

© 2022 - 2024 — McMap. All rights reserved.