5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UICollectionView(UITableView)を実装するときのDisposeBagの正しい使い方

Last updated at Posted at 2018-03-26

RxSwift使い始めの頃、DisposeBagはおまじないのように書いてるだけだったのですが、UICollectionViewを実装するときにセルに置いていたボタンのタップイベントが無駄に多く発火していることに気づき、調べた結果DisposeBagを正しく使えていなかったことが原因だとわかったので、ここにまとめておきます

ViewControllerのdisposeBagの使い回し

セルに配置しているボタンをタップしたらViewModelのbuttonTapped(product:)を呼ぶという実装で、はじめはボタンタップイベントへのサブスクリプションをViewControllerのdisposeBagに渡していました。

ProductListViewController.swift
class ProductListViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!

    var vm: ProductListViewModelProtocol!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        vm.products
            .asDriver(onErrorJustReturn: [])
            .drive(
                collectionView.rx.items(
                    cellIdentifier: "ProductListCell",
                    cellType: ProductListCell.self
                )
            ) { (_, element, cell) in
                cell.button.rx.tap.asDriver()
                    .debug(element.id.description)  // デバッグ
                    .drive(onNext: { _ in
                        self.vm.buttonTapped(product: element)
                    })
                    .disposed(by: self.disposeBag)  // 問題箇所!!
            }
            .disposed(by: disposeBag)
    }

これでどんな問題が起きるかというと、デバッグログを見るとわかるのですが、画面をスクロールしてセルをどんどん表示していくと、新しいセルが表示されるたびにcell.button.rx.tapにサブスクライブしています。
以下の数字は表示しているセルの番号だと思ってください。
画面上で8個目のセルまで表示したときに、内部では10個目のセルまで設定がされているようです。

1 -> subscribed
2 -> subscribed
3 -> subscribed
4 -> subscribed
5 -> subscribed
6 -> subscribed
7 -> subscribed
8 -> subscribed
9 -> subscribed
10 -> subscribed

ここで8個目のセルのボタンをタップすると、以下のように3個目のセルも反応してしまっています。

3 -> Event next(())
vm.buttonTapped is called
8 -> Event next(())
vm.buttonTapped is called

セルのライフサイクルに合わせてdisposeBagを使う

UICollectionView(UITableView)ではメモリ効率化のためにセルを再利用するようになっています。
先程の例だと、3個目のセルと8個目のセルは同じUICollectionViewCellのインスタンスが使われているということになります。
このため、そのインスタンスのボタンタップイベントをサブスクライブしている2つのオブザーバが反応したというわけです。

この問題を解決するには、セルが再利用される際に前回のサブスクリプションをdisposeしてあげる必要があります。
改善したコードは以下の通りです。

ProductListCell
class ProductListCell: UICollectionViewCell {
    @IBOutlet weak var button: UIButton!

    var disposeBag = DisposeBag()
ProductListViewController
...省略...
        vm.products
            .asDriver(onErrorJustReturn: [])
            .drive(
                collectionView.rx.items(
                    cellIdentifier: "ProductListCell",
                    cellType: ProductListCell.self
                )
            ) { (_, element, cell) in
                cell.disposeBag = DisposeBag()

                cell.button.rx.tap.asDriver()
                    .debug(element.id.description)  // デバッグ
                    .drive(onNext: { _ in
                        self.vm.buttonTapped(product: element)
                    })
                    .disposed(by: cell.disposeBag)  // セルのdisposeBagを使う!!
            }
            .disposed(by: disposeBag)

セルのクラスにdisposeBagプロパティを宣言し、ViewControllerでセルを再利用するときにdisposeBagを初期化してあげます。
これによってもとのdisposeBagオブジェクトはどこからも参照されなくなり、このdisposeBagオブジェクトが管理しているサブスクリプションは全てdisposeされます。
先程の例でいうと、8個目のセルが表示されるとき、3個目のセルのボタンに対してのサブスクリプションはdisposeされるようになります。

ログを確認してみる以下のようになります。
セルが再利用されるとき、サブスクリプションはdisposeされます。
先ほどと同じように8個目のセルのボタンをタップすると、1回だけbuttonTapped(product:)が呼ばれているのがわかります。

1 -> subscribed
2 -> subscribed
3 -> subscribed
4 -> subscribed
5 -> subscribed
1 -> isDisposed
6 -> subscribed
2 -> isDisposed
7 -> subscribed
3 -> isDisposed
8 -> subscribed
4 -> isDisposed
9 -> subscribed
9 -> isDisposed
5 -> subscribed
5 -> isDisposed
9 -> subscribed
5 -> isDisposed
10 -> subscribed
8 -> Event next(())
vm.buttonTapped is called

まとめ

上述のようにDisposeBagは正しく使わないと問題を引き起こします。
無駄にイベントに反応してしまうだけでなく、リソースが解放されないためメモリリークの原因にもなります。

ちなみに、この問題に対応する際に知ったdebugオペレータは非常に便利なので、是非活用してみてください。

5
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?