概要
Apple純正のNote(メモ)アプリを見ると、UISplitViewController
を利用したMaster-DatailアプリケーションでUITableView
において削除/挿入/移動が生じた際に、その後適切なセルが再選択され、secondaryViewControllerが対応したデータにreplaceされます。まるで標準挙動かのような自然な動きに感じますが、これはAppleNoteチームの独自実装のため、私たちも自前で実装するしかありません。この記事では、UITableView編集後の再選択
に対する知見と参考の実装を共有します。
目次
APIの分類
基本的にAPIはEditMode用、非EditModeの二種類が必要になります。
そもそも再選択とはどのようなものかというと、基本的には「選択中のセル削除されたとき」に、現在のテーブルの位置関係から、次の最も妥当なセルを選択するというものです。
ただし、EditModeによってその扱いが変わるため、それぞれなぜ必要なのかについて解説していきます。
非EditMode
EditModeではないとき、TableViewで気にすべきなのは「移動」「削除」の2つの更新です。削除はイメージ通りですが、移動はinsertRows
とdeleteRows
の組み合わせから実現されるため、選択中のセルが削除されることがあり、その場合に再選択が必要になります。
EditMode
EditModeに入ってしまうとシステム側でUITableViewのselectedRowIndexPaths
が一旦クリアされてしまうため、非EditModeの「削除」「移動」だけではなく全てのイベントにおいて再選択が必要になります。これについては次の章で詳しく解説します。
UITableViewのselectionとEditModeに関する考慮点
UITableView
ではsetEditing(true, animated: animated)
が呼ばれた際にセレクションをクリアする挙動がありますこれはEditModeを抜けても復帰されない上に、モード中のselectRow
の実行は無視されます。
そのため、どのような編集アクションが実行されようと、また何も実行されない場合でも、一度EditModeに入った場合は抜けた後に再選択をさせる必要があります。
クリアの挙動が実装されているのは、以下のような理由のためではないかと推察しています。
-
UITableView
のallowsMultipleSelectionをtrue
にすることでeditMode中に複数選択が可能であり、indexPathForSelectedRows
はそれらの格納に利用されるため
サンプルコード
以下のようなコードを書いて実行した際、ViewController
のコメント該当部分をコメントアウトするとindexPathForSelectedRow
が得られますが、tableView
がeditModeに入ってしまうとindexPathForSelectedRow
がnil
になってしまう、つまりセレクションがクリアされます。
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
navigationItem.rightBarButtonItem = editButtonItem
}
override func setEditing(_ editing: Bool, animated: Bool) {
print("before: \(tableView.indexPathForSelectedRow)")
super.setEditing(editing, animated: animated)
// 以下の行をコメントアウトすると、セレクションが残ります
tableView.setEditing(editing, animated: animated)
print("after: \(tableView.indexPathForSelectedRow)")
}
}
extension ViewController : UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
}
extension ViewController : UITableViewDelegate {
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
setEditing(true, animated: false)
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
setEditing(false, animated: false)
}
}
ここでは「EditMode
経由だとそれまでの選択がクリアされてされてしまうので、適切な選択をさせるならsetEditing(false)
のタイミングでselectRow
を実行する必要があるだろう」と考えることが出来ます。
方針の検討
ここで基本的な対応の方針を整理します。
上で説明した通り、EditMode
を経由すると選択がクリアされてしまい、途中の適応は無視されるため、モードを抜けるタイミングで適応させる必要があります。加えて、通常操作によるセルの更新の場合にも再選択をさせる必要があります。まとめると
-
setEditing
をoverride
して呼ぶ、EditMode用の変更を内部でpendingさせて、抜けるタイミングで最後の状態の再選択を適応できるAPI - 通常変更の差分適応をする際に再選択を行う、非EditMode用の即時再選択を適応できるAPI
の2種類のAPIに対応する必要があります。
API設計
これに対して、このようなAPIを提案します。
reselect()
setReselect()
commit()
1つ目は即時実行用、2つ目と3つ目はEditMode中の複数の変更に対応するためのAPIになります。
実装
こちらに実装しました。
内部的にpendingItemForSelectedRow
というぶら下がったプロパティを持つことで、EditMode中の複数の変更でも再選択のための適切なIndexPathを計算・保持し続けることができます。
reselect
では、以下のように内部的にはsetReselect
を上手く呼ぶことで無駄な実装を省くことに成功しました。
public func reselect(deletedIndexPaths: [IndexPath]? = nil) {
refresh() // 内部変数のリフレッシュ
setReselect(deletedIndexPaths: deletedIndexPaths)
commit()
}
サンプル
利用サンプルは後日、github側に追加して解説を入れます。
まとめ
「適切なセルの再選択」というのは、UITableViewの標準挙動で提供されていそうなものですが、実現されていません。
そのため、この記事では
- EditMode
- 非EditMode
用に分けて理想挙動を解説し、EditMode時のselectionの考慮点について述べました。
その後、APIのインターフェースについて提案しつつ、筆者の実装例とサンプルを示しました。
展望としては、記事化とOSS化によって筆者が未考慮の事項がコメントやPRで集まり、さらに良い再選択のロジックについて議論・構築が進めば良いなと考えております。