【RxSwift】TableViewのCell内にUISwitchを配置する

# はじめに
RxSwiftを使ってTableViewのCellに配置したUISwitchを操作しようとしたところ、想定外の挙動に悩まされたので、ハマったポイントをまとめてみました

# 環境
Xcode 13.3
Swift 5.6
RxSwift 6.2.0

# 内容

RxSwiftを使いましたが複数セクション&複数カスタムセルを表示する必要があったため、
`func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell`を使いました
コードは対象箇所のみにしたため、分かりにくいかもしれません

このような画面の実装をしました（他にもセクションが存在します）
<img width="300" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/96e9658c-3d67-e3df-0b10-ee1623afcaaa.png">

## 実装パターン① : CustomCell内に処理を書く

```swift: UITableViewCell
class CustomCell: UITableViewCell {
    @IBOutlet private weak var playerLabel: UILabel!
    @IBOutlet private weak var playerSwitch: UISwitch!
    var disposeBag = DisposeBag()

    override func prepareForReuse() {
        super.prepareForReuse()
        disposeBag = DisposeBag()
    }    
 
    func render(player: Player, delegate: TableViewCellDelegate) {
        self.playerLabel.text = player.name
        self.playerLabel.textColor = player.gender ? .label : .red

        playerSwitch.setOn(player.isPlaying, animated: false)

        playerSwitch.rx.controlEvent(.valueChanged)
            .withLatestFrom(playerSwitch.rx.value)
            .subscribe(onNext : { isPlaying in
                 delegate.handlePlayerSwitchChanged(playerId: player.playerId, isPlaying: isPlaying)
            }).disposed(by: disposeBag)
    }
}

```

```swift: MainViewController

extension MainViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.customCell, for: indexPath)!

        cell.render(player: viewModel.playerList.value[indexPath.row], delegate: self)
        return cell
    }
}

extension MainViewController: TableViewCellDelegate {
    func playerSwitchChanged(playerId: Int16, isPlaying: Bool) {
        viewModel.handlePlayerSwitchChanged(playerId: playerId, isPlaying: isPlaying)
    }
}

```

`delegate`を実装する必要がありますが、UISwitchの処理をセル側に書けるので少し読みやすくなるかと思います
セル側の`prepareForReuse`で一度disposeする必要があります

## 実装パターン② : ViewController内に処理を書く

```swift: UITableViewCell
class CustomCell: UITableViewCell {
    @IBOutlet private weak var playerLabel: UILabel!
    @IBOutlet weak var playerSwitch: UISwitch!
    var disposeBag = DisposeBag()

    override func prepareForReuse() {
        super.prepareForReuse()
        disposeBag = DisposeBag()
    }    
 
    func render(player: Player) {
        self.playerLabel.text = player.name
        self.playerLabel.textColor = player.gender ? .label : .red

        playerSwitch.setOn(player.isPlaying, animated: false)
    }
}

```

```swift: ViewController
extension MainViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.customCell, for: indexPath)!

        let player = viewModel.playerList.value[indexPath.row]
        render(player: player)

        cell.playerSwitch.rx.controlEvent(.valueChanged)
            .withLatestFrom(playerSwitch.rx.value)
            .subscribe(onNext : { [weak self] isPlaying in
                 guard let self = self else { return }
                 viewModel.handlePlayerSwitchChanged(playerId: player.playerId, isPlaying: isPlaying)
            }).disposed(by: cell.disposeBag)
        return cell
    }
}
```

セル内にUISwitchの処理を書かない場合は`delegate`の実装が不要になります
しかし複数セクション・複数カスタムセルがある場合は、パターン①の方が良さそうです
こちらもセル側の`prepareForReuse`で一度disposeする必要があります

## ハマったポイント

- UISwitchの`.isSelected = `で値をセットした
これだと値をセットしたタイミングで値が変わったとカウントされるので、`.setOn`を使うべきでした

- `.rx.isOn`をsubscribeしていた
値が変更されたときのみ、処理を流すようにするべきでした
```swift
        playerSwitch.rx.controlEvent(.valueChanged)
            .withLatestFrom(playerSwitch.rx.value)
            .subscribe(onNext : { isPlaying in
```

- Cellに値を持たせた
`BehaviorRelay`などを使いCellに値を持たせたところ`prepareForReuse`でdisposeしても想定外の挙動が起こりました。やり方が悪かったのかもしれませんが、よく考えたら値を持たせる必要がなかったのでやめました

# おわりに
RxSwiftの理解が足りてない以前にUISwitchの挙動も理解が足りてなく、思わぬところでハマってました。
困ったら`prepareForReuse`で一度disposeすれば全てOKだと思ってたところもあり、もう少し理解を深める必要がありそうです。


# 参考

https://qiita.com/sho_v00/items/1319ae56d3ca60ab104e

https://qiita.com/katafuchix/items/5909fa2d38b5f5455df1

https://stackoverflow.com/questions/51206550/switch-button-on-tableview-cell-rxswift

https://github.com/RxSwiftCommunity/RxDataSources/issues/119

https://tapdev.co/2021/03/15/how-to-bind-button-taps-in-custom-cells-to-your-view-model/

https://tech.gunosy.io/entry/sofubokoukou_rx_swift

https://qiita.com/katafuchix/items/77adff07609e8f580fcb

https://qiita.com/shori_m/items/05417c97b7dfe8da52d2