UITableViewCell のタップした位置の IndexPath を取得する
検証環境:
Xcode 11.3
Swift 5.1.3
UITableViewCell 上に置いたボタンをタップしたときにそのセルをアニメーション削除したい。
UITableViewDataSource プロトコルの cellForRowAt メソッドにセルのコールバックプロパティに引数の indexPath を渡す実装にしたところ、セル削除時に Index out of range が発生してクラッシュした。
原因はキャッシュされた indexPath を使っているためで、例えば2つセルがあって、先に1つ目を消し、その後2つ目を消すと存在しない index を指定することになるため。
修正前
// セル class MyCell: UITableViewCell { var tapButtonHandler: (() -> Void)? @IBAction func handleButton(_ sender: UIButton) { tapButtonHandler?(event) } } // UITableViewDataSource func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath) as! MyCell cell.tapButtonHandler = { [weak self] in self?.items.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .fade) } return cell }
タップした位置に存在する IndexPath を取得するように修正した。
- UIEvent から UITouch を取得
- UITouch の locationInView メソッドで TableView 上の CGPoint 取得
- UITableView の indexPathForRowAtPoint メソッドで CGPoint が含まれる IndexPath 取得
という流れ。
修正後
// セル class MyCell: UITableViewCell { var tapButtonHandler: ((UIEvent) -> Void)? @IBAction func handleButton(_ sender: UIButton, event: UIEvent) { tapButtonHandler?(event) } } // UITableViewDataSource func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath) as! MyCell cell.tapButtonHandler = { [weak self] event in guard let self = self else { return } if let touch = event.allTouches?.first { let point = touch.location(in: self.tableView) if let selectedIndexPath = tableView.indexPathForRow(at: point) { self.items.remove(at: selectedIndexPath.row) tableView.deleteRows(at: [selectedIndexPath], with: .fade) } } } return cell }