環境
- Xcode11.3
- Swift5.1.3
経緯
とあるTableViewでセクションによって単一選択か複数選択かを分岐したい時ってありますよね。そうですよね、ええ、はい、あんまりないですよね。最近、初めて出会いました。
経験浅なので、セクション毎の設定とかできるんでしょうくらいに思っていたら、そんなものはなく、中々苦労したので今回取り上げました。
ひとまず完成GIF
銭湯セクションのみ単一選択で、他のセクションは複数選択で設定しています。
銭湯の選出については適当です(おすすめがあればコメントください)。
実装方法
今回の表題に関係のない箇所は省きます。
一応Repositoryのリンクは載せておきます。
SingleAndMultipleSelectionTableView
TableViewの設定
必要なのはこれだけです。
defaultではfalseになっているのでtrueにしてあげます。
override func viewDidLoad() {
super.viewDidLoad()
// UITableView全体は複数選択可能に設定
tableView.allowsMultipleSelection = true
}
UITableViewDataSourceとUITableViewCell
UITableViewDataSource
ここで出てくるcellTitles
はViewControllerのメンバ変数で各Cellのタイトルを格納している多次元配列です。該当箇所はこちら
withIdentifierに渡しているCustomCell
についてはこの後に説明します。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath)
cell.textLabel?.text = cellTitles[indexPath.section][indexPath.row]
cell.selectionStyle = .none
return cell
}
UITableViewCell
先ほど出てきたCustomCellです。
自前でsetSelected(_:animated:))
を記述したカスタムCellを用意すると、Cellの生成時orタップ時、よしなにaccessoryType
を管理してくれます。
初めはDelegateメソッドのtableView(_:didSelectRowAt:)
とtableView(_:didDeselectRowAt:)
に各々処理を書いていましたが、Cellの生成(かつリユース)時の処理も考えるとコード量も増えてしまいます。
これならコードスッキリトテモイイネ。
private final class CustomCell: UITableViewCell {
// Cellが生成されるタイミングでselectedの状態がtrueならチェックを付ける
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
accessoryType = selected ? .checkmark : .none
}
}
....あれ、なんか違う...
UITableViewDelegate
これですね。Delegate。本命はここ、ここを実装しない限り表題の実現は叶いません。
コメントアウトにある程度説明を書いてしまっていますが、、
tableView(_:willSelectRowAt)
で、Cellが選択されようとしている時に、単一セクション内のCellであれば排他制御処理を行います。
tableView(_:willDeselectRowAt)
も同様です。
extension SingleAndMultipleSelectionTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
// SingleSelectionSectionであれば処理を通す
guard sections[indexPath.section] == "お好きな銭湯" else { return indexPath }
// tableViewから選択されているIndexPathの配列を取得
guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else { return indexPath }
selectedIndexPaths.filter {
// 選択したCell以外で既に選択されているIndexPathに絞る
sections[$0.section] == "お好きな銭湯" && $0 != indexPath
}.forEach {
// 非選択状態にする
tableView.deselectRow(at: $0, animated: true)
}
return indexPath
}
func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
// 既に選択済みのCellであればnilを返して非選択処理を弾く
return sections[indexPath.section] == "お好きな銭湯" ? nil : indexPath
}
}
tableView(_:willSelectRowAt)
まず、どのセクションが単一選択か、そうではないのかを決めるguard文を記述します。
複数選択にしたいセクション内のindexPathが流れてきたらそのまま返します。
// SingleSelectionSectionであれば処理を通す
guard sections[indexPath.section] == "お好きな銭湯" else { return indexPath }
お次はこちら
// tableViewから選択されているIndexPathの配列を取得
guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else { return indexPath }
selectedIndexPaths.filter {
// 選択したCell以外で既に選択されているIndexPathに絞る
sections[$0.section] == "お好きな銭湯" && $0 != indexPath
}.forEach {
// 非選択状態にする
tableView.deselectRow(at: $0, animated: true)
}
tableView.indexPathsForSelectedRows
で、tableViewから選択されているIndexPathを取得します。この時取得できるindexPath郡はArray型[IndexPath]
で返ってきます。
そして取得したindexPath達を次のようにしていきます。 (語彙力)
.filter {
// 選択したCell以外で既に選択されているIndexPathに絞る
sections[$0.section] == "お好きな銭湯" && $0 != indexPath
}
単一セクション内で選択されようとしているindexPath 以外のindexPathを抽出します。(既に単一セクション内で選択されていて、選択されようとしているindexPathとは異なるindexPathのことです)
そして、 (思考力)
forEach {
// 非選択状態にする
tableView.deselectRow(at: $0, animated: true)
}
先ほど抽出したindexPath郡を全て非選択状態にします。
上記の一連の処理で、単一セクション内のindexPathに該当するCellが選択されようとした時のみ、
そのセクション内で他のindexPathに該当するCellが非選択状態に切り替わるような排他制御が実現されます。
tableView(_:willDeselectRowAt)
func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
// 既に選択済みのCellであればnilを返して非選択処理を弾く
return sections[indexPath.section] == "お好きな銭湯" ? nil : indexPath
}
コメントにある通りです。全てコメントアウトに書きました。
後記
今回初めてQiitaに投稿しましたが、こういう記事・ブログ的なのを書くのは、高校生の時にアメブロ(黒歴史)ぶりなのでそわそわしています。これから少しずつ記事を書いていきたいと思います。
文化浴泉とてもいいです。三茶の八幡湯と北欧は最近行きました。北欧はズルイですね。