1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift/RxSwift] RxCocoa拡張機能を利用しTableView表示・選択の実装

Last updated at Posted at 2025-09-09

RxCocoaの拡張機能

今回はRxCocoaでの拡張機能を紹介します。
TableViewの表示や選択する機能の実装の仕方が通常のものと違いますので、備忘録的にまとめていきます。

実装例

Ninja
import Foundation

struct Ninja {
    let name: String
    let village: String 
    let specialJutsu: String 
}
NinjaViewModel
import Foundation
import RxSwift
import RxCocoa

protocol NinjaViewModelInputs: AnyObject {
    var select: PublishRelay<[Ninja]> { get }    // 複数選択をまとめて送る
    var deselect: PublishRelay<[Ninja]> { get }  // 複数解除をまとめて送る
}

protocol NinjaViewModelOutputs: AnyObject {
    var ninja: Driver<[Ninja]> { get }           // 一覧表示用
    var selected: Driver<[Ninja]> { get }        // 現在選択中まとめ
}

protocol NinjaViewModelType: AnyObject {
    var inputs: NinjaViewModelInputs { get }
    var outputs: NinjaViewModelOutputs { get }
}

final class NinjaViewModel: NinjaViewModelType, NinjaViewModelInputs, NinjaViewModelOutputs {

    // MARK: - IO
    var inputs: NinjaViewModelInputs { return self }
    var outputs: NinjaViewModelOutputs { return self }

    // MARK: - Inputs
    var select   = PublishRelay<[Ninja]>()
    var deselect = PublishRelay<[Ninja]>()

    // MARK: - Outputs
    let ninja: Driver<[Ninja]>
    let selected: Driver<[Ninja]>

    // MARK: - Data Source
    let ninjas: [Ninja] = [
        Ninja(name: "たろう", village: "東", specialJutsu: "手裏剣術"),
        Ninja(name: "はなこ", village: "西", specialJutsu: "変わり身の術"),
        Ninja(name: "けんじ", village: "西", specialJutsu: "隠れ身の術"),
        Ninja(name: "みどり", village: "南", specialJutsu: "分身の術"),
        Ninja(name: "ゆき", village: "北", specialJutsu: "火遁の術"),
        Ninja(name: "そら", village: "南", specialJutsu: "水遁の術"),
        Ninja(name: "ひかり", village: "東", specialJutsu: "雷遁の術")
    ]

    // MARK: - State
    private let _ninja = BehaviorRelay<[Ninja]>(value: [])
    private let _selected = BehaviorRelay<[Ninja]>(value: [])
    private let disposeBag = DisposeBag()

    // MARK: - Init
    init() {
        // 一覧
        _ninja.accept(ninjas)
        ninja = _ninja.asDriver(onErrorDriveWith: .empty())
        selected = _selected.asDriver(onErrorDriveWith: .empty())

        // 追加:現在選択に複数を重複なしで追加
        select.asObservable()
            .withLatestFrom(_selected.asObservable()) { incoming, current in
                var ids = Set(current.map { $0.name })
                incoming.forEach { ids.insert($0.name) }
                return self.ninjas.filter { ids.contains($0.name) }
            }
            .bind(to: _selected)
            .disposed(by: disposeBag)

        // 解除:現在選択から複数を解除
        deselect
            .withLatestFrom(_selected.asObservable()) { removing, current in
                let removeIDs = Set(removing.map { $0.name })
                return current.filter { !removeIDs.contains($0.name) }
            }
            .bind(to: _selected)
            .disposed(by: disposeBag)
    }
}

NinjaTableViewController
import UIKit
import RxSwift
import RxCocoa

final class NinjaTableViewController: UITableViewController {

    @IBOutlet private var ninjaTableView: UITableView! {
        didSet {
            ninjaTableView.register(UINib(nibName: "NinjaTableViewCell", bundle: nil),
                                    forCellReuseIdentifier: "NinjaTableViewCell")
            ninjaTableView.rowHeight = UITableView.automaticDimension
            ninjaTableView.estimatedRowHeight = 24
        }
    }
    private let viewModel = NinjaViewModel()
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        ninjaTableView.dataSource = nil
        // TableViewCell 複数選択
        ninjaTableView.allowsMultipleSelection = true
        bind()
    }
}

extension NinjaTableViewController {
    func bind() {
        viewModel.outputs.ninja
            .drive(ninjaTableView.rx.items) { tableView, row, element in
                let indexPath = IndexPath(row: row, section: 0)
                guard let cell = tableView.dequeueReusableCell(withIdentifier: "NinjaTableViewCell", for: indexPath) as? NinjaTableViewCell else {
                    return UITableViewCell()
                }
                cell.configure(with: element)
                return cell
            }
            .disposed(by: disposeBag)

        // 1件登録
        ninjaTableView.rx.modelSelected(Ninja.self)
            .map { [$0] }
            .bind(to: viewModel.inputs.select)
            .disposed(by: disposeBag)

        // 解除
        ninjaTableView.rx.modelDeselected(Ninja.self)
            .map { [$0] }
            .bind(to: viewModel.inputs.deselect)
            .disposed(by: disposeBag)

        viewModel.outputs.selected
            .drive(onNext: { print("データは?:\($0)") })
            .disposed(by: disposeBag)
    }
}

NinjaTableViewCell
import UIKit

final class NinjaTableViewCell: UITableViewCell {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var villageLabel: UILabel!
    @IBOutlet private weak var specialJutsuLabel: UILabel!

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        contentView.backgroundColor = selected ? UIColor.systemIndigo.withAlphaComponent(0.6) : .clear
        nameLabel.textColor = selected ? .white : .label
        villageLabel.textColor = selected ? .white : .secondaryLabel
        specialJutsuLabel.textColor = selected ? .white : .secondaryLabel
    }

    func configure(with ninja: Ninja) {
        nameLabel.text = ninja.name
        villageLabel.text = ninja.village
        specialJutsuLabel.text = ninja.specialJutsu
    }
}

RxCocoaと通常のTableViewの違い

通常のUITableView実装例

下記に特徴をまとめます。

  • numberOfRowsInSection/cellForRowAt/didSelectRowAtを実装して、表示数・セル内容・タップ時の動作を自分で定義する
  • データ更新時に画面へ反映する処理が必要
    • データ更新時にtableView.reloadData()もしくはinsertRows(at:)などの差分更新を呼ぶ
    • TableView は配列の変更を自動検知しないため、いつ描き直すかを開発者が指示する
  • ユーザー操作の受け取りは delegateメソッド(例:didSelectRowAt)で行う
    • UITableViewControllerを継承している場合は 既定でselfが delegate/dataSourceに設定される
    • UIViewController + UITableView構成ではtableView.dataSource = self / tableView.delegate = selfを自前で設定
通常のUITableView実装
final class NormalTableViewController: UITableViewController {

    var ninjas = [
        Ninja(name: "たろう", village: "東", specialJutsu: "手裏剣術"),
        Ninja(name: "はなこ", village: "西", specialJutsu: "変わり身の術"),
        Ninja(name: "けんじ", village: "西", specialJutsu: "隠れ身の術")
    ]

    // 行数
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        ninjas.count
    }

    // セル内容
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "NinjaTableViewCell", for: indexPath) as? NinjaTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: ninjas[indexPath.row])
        return cell
    }

    // タップ時の動作
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("選択されたNinja: \(ninjas[indexPath.row])")
    }

    // 例:データ更新と画面反映
    func addNinja(_ ninja: Ninja) {
        let newIndex = ninjas.count
        ninjas.append(ninja)
        tableView.reloadData()
    }
}

RxCocoaを使った実装例

下記に特徴をまとめます。

  • dataSourceもdelegate不要
  • データの更新が流れると自動的にTableViewに反映される
  • 選択イベントも.rx.modelSelectedで即取得できる
RxCocoaを使った実装
viewModel.outputs.ninja
    .drive(ninjaTableView.rx.items) { tableView, row, element in
        let indexPath = IndexPath(row: row, section: 0)
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "NinjaTableViewCell", for: indexPath) as? NinjaTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: element)
        return cell
    }
    .disposed(by: disposeBag)

ninjaTableView.rx.modelSelected(Ninja.self)
    .asDriver()
    .drive(onNext: { ninja in
        print("選択されたNinja: \(ninja)")
    })
    .disposed(by: disposeBag)

このように RxCocoaを使用したTableView の実装を行うと、非常にシンプルで保守しやすいコードにまとめられます。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?