はじめに
RxSwiftを使ってTableViewのCellに配置したUISwitchを操作しようとしたところ、想定外の挙動に悩まされたので、ハマったポイントをまとめてみました
環境
Xcode 13.3
Swift 5.6
RxSwift 6.2.0
内容
RxSwiftを使いましたが複数セクション&複数カスタムセルを表示する必要があったため、
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
を使いました
コードは対象箇所のみにしたため、分かりにくいかもしれません
このような画面の実装をしました(他にもセクションが存在します)
実装パターン① : CustomCell内に処理を書く
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)
}
}
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内に処理を書く
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)
}
}
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していた
値が変更されたときのみ、処理を流すようにするべきでした
playerSwitch.rx.controlEvent(.valueChanged)
.withLatestFrom(playerSwitch.rx.value)
.subscribe(onNext : { isPlaying in
- Cellに値を持たせた
BehaviorRelay
などを使いCellに値を持たせたところprepareForReuse
でdisposeしても想定外の挙動が起こりました。やり方が悪かったのかもしれませんが、よく考えたら値を持たせる必要がなかったのでやめました
おわりに
RxSwiftの理解が足りてない以前にUISwitchの挙動も理解が足りてなく、思わぬところでハマってました。
困ったらprepareForReuse
で一度disposeすれば全てOKだと思ってたところもあり、もう少し理解を深める必要がありそうです。
参考