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を自前で設定
- UITableViewControllerを継承している場合は 既定で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 の実装を行うと、非常にシンプルで保守しやすいコードにまとめられます。