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

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

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

```swift: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`にサブスクライブしています。
以下の数字は表示しているセルの番号だと思ってください。
画面上で８個目のセルまで表示したときに、内部では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)ではメモリ効率化のためにセルを再利用するようになっています。
先程の例だと、３個目のセルと８個目のセルは同じUICollectionViewCellのインスタンスが使われているということになります。
このため、そのインスタンスのボタンタップイベントをサブスクライブしている２つのオブザーバが反応したというわけです。

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

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

    var disposeBag = DisposeBag()
```

```swift: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されます。
先程の例でいうと、８個目のセルが表示されるとき、３個目のセルのボタンに対してのサブスクリプションはdisposeされるようになります。

ログを確認してみる以下のようになります。
セルが再利用されるとき、サブスクリプションはdisposeされます。
先ほどと同じように８個目のセルのボタンをタップすると、１回だけ`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`オペレータは非常に便利なので、是非活用してみてください。