проблема с наличием rx.tap для UIButton в UICollectionViewCell - RxSwift 3

Я подписываюсь 2 раза на 1 UIButton:

  1. Первая подписка для обновления пользовательского интерфейса при каждом клике
  2. Вторая подписка для обновления значений в веб-сервисе каждую секунду после накопления кликов.

Код:

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()
    }

}

Проблема:

Поскольку удаление происходит prepareForReuse(), если я нажимаю кнопку много раз и сразу же прокручиваю, вызовы веб-службы удаляются и не обновляются.

что я пробовал:

  1. Добавлено addDisposableTo(vc?.rx_disposableBag) в родительский ViewController DisposableBag.

    Проблема, подписки накапливаются, и при каждом нажатии много раз вызывается updateWS(), который подписывается при каждой прокрутке и никогда не удаляется.

  2. Я попытался удалить повторную инициализацию disposableBag из prepareForReuse().

    Проблема. Снова подписки на кнопки дублируются и накапливаются, а многие вызовы веб-сервисов вызываются при каждом нажатии.

Вопрос: Как я могу получить debounce подписки, вызываемые до конца и никогда не повторяющиеся с несколькими подписками (в случае addDisposableTo viewController Bag)?


person MBH    schedule 04.04.2017    source источник
comment
Не уверен в вашем случае, поэтому, пожалуйста, уточните: используется ли productionSize в вашем updateOnWS? Таким образом, пользователь может нажимать кнопку много раз, а затем, когда он перестает нажимать, номер (т.е. нажатый 5 раз) 5 используется для сетевого вызова? И каким должно быть поведение, когда вы прокручиваете вниз и удаляете ячейку; должен ли сетевой запрос быть утилизирован или он все еще должен выполняться? И должен ли пользовательский интерфейс производственного размера обновляться при касании пользователя или при успешном ответе на сетевой запрос?   -  person dsapalo    schedule 04.04.2017
comment
Кроме того, будьте осторожны с тем, как вы используете self внутри замыканий, это может привести к утечкам памяти, которые складываются, особенно когда они находятся в UITableViewCells или UICollectionViewCells. Это предотвращает удаление объекта. См. stackoverflow.com/questions/40583685/   -  person dsapalo    schedule 04.04.2017
comment
@iwillnot я обновляю productionSize каждый щелчок, чтобы при вызове веб-службы я брал последнее значение productionSize и отправлял его в updateOnWS. если нажать 5 раз, он будет обновляться только 1 раз в сети. я не жду ответа, для лучшего взаимодействия с пользователем я показываю, что пользовательский интерфейс обновляет их, я обновляю его, если запрос был успешно выполнен   -  person MBH    schedule 04.04.2017
comment
Каким должно быть поведение при прокрутке вниз и удалении ячейки; должен ли сетевой запрос быть утилизирован или он все еще должен выполняться?   -  person dsapalo    schedule 12.04.2017
comment
@iwillnot запрос должен быть выполнен   -  person MBH    schedule 12.04.2017


Ответы (2)


Поскольку prepareButton() всегда вызывается в (cellForItemAt indexPath: IndexPath) представления коллекции, вы можете попробовать следующее:

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
}

Удалите реализацию prepareForReuse().

person xandrefreire    schedule 11.04.2017
comment
Спасибо за твой ответ. похоже, что ваш код делает то же самое, что и prepareForReuse(), поскольку prepareButton вызывается каждый раз, когда ячейка повторно используется при прокрутке, так что он удаляет запрос до его завершения. в любом случае я проверю это и поделюсь результатом еще раз - person MBH; 11.04.2017
comment
снова запрос был отменен очень рано, если я нажму кнопку и прокрутлю сразу, не дожидаясь времени отката (1 сек). - person MBH; 12.04.2017

Добавлен addDisposableTo(vc?.rx_disposableBag) в родительский ViewController DisposableBag.

Проблема в том, что подписки накапливались, и при каждом щелчке много раз вызывалась функция updateWS(), которая подписывается при каждой прокрутке и никогда не удаляется.

Возможно, ваш self.updateOnWS() вызывается много раз из-за того, как вы подписываетесь на нажатие кнопки.

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

Как видите, вы подписываетесь на все события с помощью метода subscribe(). Это означает, что все события Rx (onNext, onError, onCompleted, onSubscribed и onDisposed) запускают self.updateOnWS(). Вы можете проверить, так ли это, распечатав объект event, чтобы увидеть, какое событие было вызвано.

Подписывайтесь только на onNext

Возможным решением может быть подписка только на операцию onNext.

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

Используя DisposeBag контроллера представления, вы можете убедиться, что операция все еще продолжается, даже если ячейка удаляется (при прокрутке вниз). Однако, если вам нужно удалить подписку при удалении ячейки, используйте DisposeBag ячейки, а не контроллер представления.

Боковое примечание — утечка памяти

Обратите внимание, что ссылка на self указана как слабая, чтобы вы могли предотвратить утечку памяти. Указав, что он слабый, он предоставит вам необязательную ссылку на себя.

Без этого замыкание, созданное вами для блока onNext, сохранит сильную ссылку на блок self, который является вашим UICollectionViewCell, которому, в свою очередь, принадлежит то самое замыкание, которое мы обсуждаем.

Это в конечном итоге приведет к сбою из-за нехватки памяти. См. ссылку, которую я разместил в комментариях к вашему вопросу, чтобы узнать больше об утечках памяти, вызванных неправильной ссылкой на себя.

person dsapalo    schedule 04.04.2017
comment
Большое спасибо за ваш ответ. Было приятно узнать об утечке памяти, но есть большая проблема, когда я подписываюсь на .addDisposableTo(vc?.rxdisposableBag) . старые подписки не удаляются при прокрутке, в них накапливается много подписок и много вызовов, реализованных в веб-сервисе. - person MBH; 05.04.2017
comment
мы можем избавиться, не отменяя старый вызов? - person MBH; 05.04.2017
comment
Вы можете вручную обрабатывать свои подписки, вызывая dispose для объекта подписки. Или у вас есть DisposeBag, который вы можете повторно создать, чтобы имитировать удаление. - person dsapalo; 05.04.2017
comment
какое вообще может быть другое решение этой проблемы? - person MBH; 05.04.2017
comment
Я бы подумал об использовании операторов debounce, throttle и даже, возможно, посмотрел бы, будет ли buffer с filter с условием count > 0 вести себя так, как я хочу. Все, что, вероятно, могло бы уменьшить многократное выполнение кода подписки. Для удаления старых подписок я бы использовал отдельный пакет удаления, который я очищаю вручную или использую оператор `flatMapLatest. Если ответ помог, пожалуйста, отметьте его как принятый. :) - person dsapalo; 05.04.2017