Rxswift: What is a clever way to avoid duplicate subscriptions to an Observable ?

Created on 6 Aug 2016  路  11Comments  路  Source: ReactiveX/RxSwift

When we are using RxactiveX with UITableView, we may need to subscribe to observables in each cell. As cell are reused, there are chances that we will subscribe an Observable more than once on a cell.

Off course we can place a Disposable in the cell and check it each time when we want to subscribe an observable in this cell. But is it an easier way to do that ?

Most helpful comment

For anyone interested, here's the code with same concepts as above, but works universally on any Objective-C class that implements prepareForReuse method:

import UIKit
import RxCocoa
import RxSwift

private var prepareForReuseBag: Int8 = 0

@objc public protocol Reusable : class {
    func prepareForReuse()
}

extension UITableViewCell: Reusable {}
extension UITableViewHeaderFooterView: Reusable {}
extension UICollectionReusableView: Reusable {}

extension Reactive where Base: Reusable {
    var prepareForReuse: Observable<Void> {
        return Observable.of(sentMessage(#selector(Base.prepareForReuse)).map { _ in }, deallocated).merge()
    }

    var reuseBag: DisposeBag {
        MainScheduler.ensureExecutingOnScheduler()

        if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {
            return bag
        }

        let bag = DisposeBag()
        objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

        _ = sentMessage(#selector(Base.prepareForReuse))
            .subscribe(onNext: { [weak base] _ in
                let newBag = DisposeBag()
                objc_setAssociatedObject(base, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
            })

        return bag
    }
}

All 11 comments

Hi @jibeex ,

please take a look at RxExample app for ideas. I believe there are also some pods that add dispose bag functionality to UITableViewCell.

I also use something like this

import RxSwift

private var prepareForReuseBag: Int8 = 0

extension UITableViewCell {
    var rx_prepareForReuse: Observable<Void> {
        return Observable.of(self.rx_sentMessage(#selector(UITableViewCell.prepareForReuse)).map { _ in () }, self.rx_deallocated).merge()
    }

    var rx_prepareForReuseBag: DisposeBag {
        MainScheduler.ensureExecutingOnScheduler()

        if let bag = objc_getAssociatedObject(self, &prepareForReuseBag) as? DisposeBag {
            return bag
        }

        let bag = DisposeBag()
        objc_setAssociatedObject(self, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

        _ = self.rx_sentMessage(#selector(UITableViewCell.prepareForReuse))
            .subscribeNext { [weak self] _ in
                let newBag = DisposeBag()
                objc_setAssociatedObject(self, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
            }

        return bag
    }
}

Thank your for the really nice idea !

Swift3.0 version:

```swift
private var prepareForReuseBag: Int8 = 0

extension Reactive where Base: UITableViewCell {
var prepareForReuse: Observable {
return Observable.of(sentMessage(#selector(UITableViewCell.prepareForReuse)).map { _ in () }, deallocated).merge()
}

var disposeBag: DisposeBag {
    MainScheduler.ensureExecutingOnScheduler()

    if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {
        return bag
    }

    let bag = DisposeBag()
    objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

    _ = sentMessage(#selector(UITableViewCell.prepareForReuse))
        .subscribe(onNext: { [weak base] _ in
            let newBag = DisposeBag()
            objc_setAssociatedObject(base, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        })

    return bag
}

}

extension Reactive where Base: UITableViewHeaderFooterView {
var prepareForReuse: Observable {
return Observable.of(sentMessage(#selector(UITableViewHeaderFooterView.prepareForReuse)).map { _ in () }, deallocated).merge()
}

var disposeBag: DisposeBag {
    MainScheduler.ensureExecutingOnScheduler()

    if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {
        return bag
    }

    let bag = DisposeBag()
    objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

    _ = sentMessage(#selector(UITableViewHeaderFooterView.prepareForReuse))
        .subscribe(onNext: { [weak base] _ in
            let newBag = DisposeBag()
            objc_setAssociatedObject(base, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        })

    return bag
}

}```

@farzadshbfn nice!

Found a bug in your code. You are associating the disposeBag with self instead of base. That code creates a new disposeBag everytime

if let bag = objc_getAssociatedObject(base, &prepareForReuseBagKey) as? DisposeBag {

@jinthagerman Yeah you're right, I did fix this problem in my code after a while noticing increase in resources, but forgot to update here too. I updated the answer ;) tnx 馃憤馃徏

For anyone interested, here's the code with same concepts as above, but works universally on any Objective-C class that implements prepareForReuse method:

import UIKit
import RxCocoa
import RxSwift

private var prepareForReuseBag: Int8 = 0

@objc public protocol Reusable : class {
    func prepareForReuse()
}

extension UITableViewCell: Reusable {}
extension UITableViewHeaderFooterView: Reusable {}
extension UICollectionReusableView: Reusable {}

extension Reactive where Base: Reusable {
    var prepareForReuse: Observable<Void> {
        return Observable.of(sentMessage(#selector(Base.prepareForReuse)).map { _ in }, deallocated).merge()
    }

    var reuseBag: DisposeBag {
        MainScheduler.ensureExecutingOnScheduler()

        if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {
            return bag
        }

        let bag = DisposeBag()
        objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

        _ = sentMessage(#selector(Base.prepareForReuse))
            .subscribe(onNext: { [weak base] _ in
                let newBag = DisposeBag()
                objc_setAssociatedObject(base, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
            })

        return bag
    }
}

@DenHeadless @farzadshbfn Call me a noob but I can't get this working 馃槅 ..

This should work out of the box as long and the extension is compiled as well right?

final class CarCell: UITableViewCell {

    override func awakeFromNib() {
        super.awakeFromNib()

        let editButton = UIButton(frame: CGRect(x: 0, y: 0, width: 22, height: 22))
        editButton.rx.tap
            .bind{ [weak self] in
                 // do something
            }
            .disposed(by: disposeBag) // or reuseBag in DenHeadless' example

        editingAccessoryView = editButton
    }
}

Well, if you use my version of code, you probably should use .disposed(by: rx.reuseBag) instead of disposed(by: disposeBag). Also, editingAccessoryView should be visible only in editing mode of UITableView. Other than that, looks fine, depends on your goals and what specifically does not work.

Of course I did not think about the rx. part of rx.reuseBag or rx.disposeBag.

Check. Makes sense.

Thanks and kudo's for the quick reply!

Hi everyone, I was wondering, why didn't we implement reusable disposeBag's like this?

protocol Reusable where Self: UIView {
    static var reuseIdentifier: String { get }
}

@objc private protocol ReusablePrepareable: class {
    func prepareForReuse()
}

extension UITableViewCell: Reusable, ReusablePrepareable { }
extension UITableViewHeaderFooterView: Reusable, ReusablePrepareable { }
extension UICollectionReusableView: Reusable, ReusablePrepareable { }

extension Reactive where Base: ReusablePrepareable {
    var prepareForReuse: Observable<Void> {
        return sentMessage(#selector(Base.prepareForReuse)).map { _ in }
    }
}

private var associatedDisposeBag: Int8 = 0

extension Reusable where Self: ReusablePrepareable {
    var disposeBag: DisposeBag {
        MainScheduler.ensureExecutingOnScheduler()

        if let bag = objc_getAssociatedObject(self, &associatedDisposeBag) as? DisposeBag {
            return bag
        }

        let bag = DisposeBag()
        objc_setAssociatedObject(self, &associatedDisposeBag, bag, .OBJC_ASSOCIATION_RETAIN)
        rx.prepareForReuse
            .subscribe(onNext: { [weak self] _ in
                objc_setAssociatedObject(self, &associatedDisposeBag, nil, .OBJC_ASSOCIATION_RETAIN)
            })
            .disposed(by: bag)

        return bag
    }
}

I actually don't care about the fact that I'm putting my disposeBag inside the Base class itself, (it just sounded more logical, because disposeBag is not a Reactive variable). But what got me thinking was, why we used Observable.of or Single subscription on rx.prepareForReuse (or why we didn't use rx.prepareForReuse in the code and we used sentMessage again).
But this way the whole stream, connects to a bag, which on prepareForReuse, we loose our access to the bag and no one else has any accesses to it so it get's disposed. I'm trying to figure-out why we did implement reuseBag the way it's on the comments. Thanks

And one more thing that I noticed yesterday, I think there "MIGHT" be some cases that this code doesn't work (both what I suggested, and what was discussed earlier)

If i'm right, this is the procedure of dequeueing a cell:

  1. prepareForReuse
  2. cell configuration inside the dataSource delegate which we will probably set a hook let's say:
let cell = tableView.dequeue(/* ... */) as? CustomCell
cell.button.rx.tap.subscribe(/* ... */).disposed(by: cell.rx.disposeBag) // in my code cell.disposeBag
return cell

sentMessage triggers every-time prepareForReuse gets called, in both our sample codes, there's no guarantee that resetting the DisposeBag inside the getter of disposeBag (or reuseBag) gets called sooner than cell.rx.disposeBag. so we "MIGHT" assign a bag for disposal which will be shorty deleted.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gaudecker picture gaudecker  路  3Comments

dmial picture dmial  路  3Comments

apoloa picture apoloa  路  3Comments

Z-JaDe picture Z-JaDe  路  3Comments

marlowcharite picture marlowcharite  路  3Comments