はじめに (対象読者・この記事でわかること)

この記事は、SwiftでiOSアプリを開発している中級者の方を対象にしています。特にTableViewのスワイプ削除機能を実装中に、なぜか「attempt to delete row from invalid index path」エラーが出て困っている方に最適です。

この記事を読むことで、TableViewの削除処理におけるデータソースとUIの整合性の取り方、上記エラーの根本的な原因と確実な回避策、そして安全な削除処理のパターンを習得できます。実務で発生しがちな落とし穴を事前に塞ぎ、リリース後のクラッシュをゼロにしましょう。

前提知識

この記文を読み進める上で、以下の知識があるとスムーズです。 - Swiftの基本的な文法とUIKitの扱い - TableViewのdataSource・delegateプロトコルの実装経験 - 配列の削除処理(remove(at:))の基礎

なぜ「削除」だけでエラーが起きるのか

TableViewにおけるセルの削除は、一見シンプルな操作に見えますが、実は「UI」と「データソース」の二つのレイヤーを完全に同期させる必要があるため、非常にデリケートな処理です。単純に「配列から要素を削除して、deleteRowsを呼ぶ」だけでは済まず、タイミングやスレッド、インデックスの整合性が要求されます。

特にdiffable dataSourceが登場する前の従来のdataSource方式で、スワイプ削除を実装する場合、以下の3つの要素を同時に制御する必要があります。

  1. 実際のデータ配列の状態
  2. TableViewが認識しているセルの存在
  3. アニメーション中のセルの有無

この3つがズレると、まさに「invalid index path」というエラーメッセージとともにクラッシュします。Xcodeのコンソールに表示されるこのメッセージは、データ側では「まだ存在する」と思っているのに、UI側では「もう存在しない」と認識している状況を指しています。

徹底解説:deleteRowsの前にやるべき5つのチェックリスト

ここからは、実際にプロジェクトで使える形で、エラーを回避するための実装を詳しく解説します。以下の手順を守るだけで、99%のdelete関連クラッシュを防げます。

ステップ1:データ配列の範囲チェックを徹底する

最も多いのは「配列の範囲外アクセス」です。deleteRowsを呼ぶ前に、必ず対象インデックスが配列の範囲内かを確認しましょう。

Swift
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { guard editingStyle == .delete else { return } // ① 範囲チェック guard indexPath.row < items.count else { print("🚨 範囲外のため削除をスキップ") return } // ② 配列から削除 items.remove(at: indexPath.row) // ③ UIをアニメーション付きで削除 tableView.deleteRows(at: [indexPath], with: .automatic) }

たった3行ですが、①のチェックを入れるだけで「Invalid index path」は激減します。これは「ユーザーの操作と非同期でデータが更新される」ケースで特に有効です。たとえば、バックグラウンドでWebSocketやタイマー経由で配列が書き換わっている状況を想定してください。

ステップ2:beginUpdates/endUpdatesで囲むことで一貫性を保証

複数のセルを同時に削除する場合や、セクションごと削除する場合は、必ず更新トランザクションで囲みます。これによりTableViewは一連の変更を「アトミック」に扱い、中途半端な状態をユーザーに見せなくて済みます。

Swift
tableView.beginUpdates() items.removeAll { $0.shouldRemove } // データを一括削除 let indexPaths = Array(0..<items.count).map { IndexPath(row: $0, section: 0) } tableView.deleteRows(at: indexPaths, with: .fade) tableView.endUpdates() // ここで一気に反映

ポイントは「beginUpdatesを呼んだ後、endUpdatesを呼ぶまでUIの更新が遅延される」ことです。これにより、配列の状態とUIの状態が完全に同期されます。

ハマった点:複数セクションでインデックスがズレる

私が最も時間を消費したのが「複数セクション」での削除でした。セクション0の削除後、セクション1以降の全てのインデックスが1つ前にズレます。これを見落とすと、明らかに存在するセルでも「invalid index path」が出ます。

例:

Swift
// セクション0で1件削除 items[0].remove(at: 0) // セクション1以降の全てのインデックスが1つ前に // この状態で「古いindexPath」を使ってdeleteRowsを呼ぶとクラッシュ

解決策:削除後に全てのインデックスを再計算

最も安全な方法は「削除対象をIDベースで保持し、削除後にTableView全体をリロード」することです。diffable dataSourceを使わない場合の私のおすすめパターンは以下です。

Swift
var pendingDeletionIDs: Set<UUID> = [] func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { guard editingStyle == .delete else { return } // 1. 削除対象のIDを保持 let targetID = items[indexPath.section][indexPath.row].id pendingDeletionIDs.insert(targetID) // 2. アニメーション付きで削除 tableView.performBatchUpdates({ items[indexPath.section].remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .automatic) }, completion: { _ in // 3. 必要に応じて整合性チェック self.pendingDeletionIDs.remove(targetID) }) }

さらに、バックグラウンドでデータ更新が走る場合は、必ずメインスレッドでUI操作を行うことを忘れないでください。

Swift
DispatchQueue.main.async { tableView.deleteRows(at: [indexPath], with: .automatic) }

まとめ

本記事では、Swift TableViewのdelete処理で「attempt to delete row from invalid index path」エラーが出る根本的な理由と、それを回避するための実装パターンを解説しました。

  • 配列の範囲チェック をdeleteRowsの前に必ず行う
  • beginUpdates/endUpdates で一貫性のある更新トランザクションを張る
  • 複数セクション ではインデックスの再計算を忘れずに
  • 非同期更新 では必ずメインスレッドでUI操作

この記事を通して、TableViewの削除処理でクラッシュしない、堅牢なiOSアプリの実装ができるようになりました。 今後は、iOS 13以降で利用可能なdiffable dataSourceを使った安全な削除処理、さらにCoreDataやCloudKitとの連携時の楽観的ロックについても記事にする予定です。

参考資料