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

この記事は、SwiftでiOSアプリ開発を始めたばかりの方や、Realmデータベースを使ったアプリ開発に興味がある方向けに書きました。特に、TableViewやCollectionViewでリスト表示を実装する際に、Realmからデータを取得して表示し、セルタップで詳細画面へ遷移させたいと考えている開発者の方に最適です。

この記事を読むことで、Realm Swiftで保存したデータをTableView/CollectionViewに表示し、indexPath.rowを使って特定のデータを取得・更新・削除する方法がわかります。また、RealmのResultsオブジェクトとindexPathの関係性、よくあるエラーとその解決方法も学べるため、スムーズな開発が可能になります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Swiftの基本的な文法 - TableViewまたはCollectionViewの基本的な使い方 - Realm Swiftの基本的なインストールとセットアップ

RealmとindexPath.rowの関係性を理解する

Realm Swiftは、モバイル向けの高速なデータベースです。特に、オブジェクト指向のデータベースとして、Swiftの配列のように扱える点が魅力です。しかし、TableViewやCollectionViewでデータを表示する際、indexPath.rowを使って特定のデータにアクセスしようとすると、配列とは少し違う挙動をすることもあります。

Realmから取得したデータはResults<T>という型で返されます。このResultsは配列のように見えますが、実際にはライブオブジェクトで、データベースの変更を自動的に反映する特殊なコレクションです。indexPath.rowを使って特定のオブジェクトにアクセスする際、このResultsの特性を理解しておくことが重要です。

実装:indexPath.rowを使ったRealmデータの操作

ここからは、実際にindexPath.rowを使ってRealmのデータを操作する方法を詳しく解説します。サンプルとして、ToDoアプリを想定して進めていきます。

ステップ1:データモデルの作成と基本的なセットアップ

まず、Realm用のデータモデルを作成します。

Swift
import RealmSwift class Task: Object { @Persisted(primaryKey: true) var id: ObjectId @Persisted var title: String = "" @Persisted var isCompleted: Bool = false @Persisted var createdAt: Date = Date() convenience init(title: String) { self.init() self.title = title } }

次に、ViewControllerでRealmインスタンスとResultsプロパティを準備します。

Swift
import UIKit import RealmSwift class TaskListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! let realm = try! Realm() var tasks: Results<Task>? override func viewDidLoad() { super.viewDidLoad() setupTableView() loadTasks() } private func setupTableView() { tableView.delegate = self tableView.dataSource = self } private func loadTasks() { tasks = realm.objects(Task.self).sorted(byKeyPath: "createdAt", ascending: false) tableView.reloadData() } }

ステップ2:indexPath.rowを使ったデータ表示と操作

TableViewのデータソースメソッドでindexPath.rowを使ってRealmデータにアクセスします。

Swift
extension TaskListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tasks?.count ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) // indexPath.rowを使ってRealmデータにアクセス if let task = tasks?[indexPath.row] { cell.textLabel?.text = task.title cell.accessoryType = task.isCompleted ? .checkmark : .none } return cell } }

セルがタップされたときの処理もindexPath.rowを使って実装します。

Swift
extension TaskListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) // indexPath.rowを使ってタスクの完了状態を切り替える if let task = tasks?[indexPath.row] { do { try realm.write { task.isCompleted.toggle() } tableView.reloadRows(at: [indexPath], with: .fade) } catch { print("Error updating task: \(error)") } } } // スワイプで削除 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { if let task = tasks?[indexPath.row] { do { try realm.write { realm.delete(task) } tableView.deleteRows(at: [indexPath], with: .fade) } catch { print("Error deleting task: \(error)") } } } } }

ハマった点:Index out of boundsエラー

実装中、特よく遭遇するのが「Index out of bounds」エラーです。これは主に以下の状況で発生します:

  1. データベースが更新された直後にTableViewをリロードしない
  2. Resultsがnilの状態でindexPath.rowにアクセスしようとする
  3. バックグラウンドスレッドからUIの更新を試みる

例えば、以下のようなコードはエラーの原因になります:

Swift
// ❌ これは危険! func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) // tasksがnilの可能性があるのに強制アンラップ let task = tasks![indexPath.row] // ここでクラッシュ! cell.textLabel?.text = task.title return cell }

解決策:安全にアクセスするためのベストプラクティス

以下のように、オプショナルバインディングと適切なエラーハンドリングを行います:

Swift
// ✅ これが推奨! class TaskListViewController: UIViewController { // ... // 通知オブザーバーを保持 var notificationToken: NotificationToken? override func viewDidLoad() { super.viewDidLoad() setupTableView() loadTasks() setupRealmNotification() } private func setupRealmNotification() { notificationToken = tasks?.observe { [weak self] changes in switch changes { case .initial: self?.tableView.reloadData() case .update(_, let deletions, let insertions, let modifications): self?.tableView.performBatchUpdates({ self?.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic) self?.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic) self?.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic) }) case .error(let error): print("Error: \(error)") } } } deinit { notificationToken?.invalidate() } } // 安全なアクセスメソッド private func task(at indexPath: IndexPath) -> Task? { guard let tasks = tasks, tasks.indices.contains(indexPath.row) else { return nil } return tasks[indexPath.row] } // 使用例 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) if let task = task(at: indexPath) { cell.textLabel?.text = task.title cell.accessoryType = task.isCompleted ? .checkmark : .none } return cell }

また、データの更新は必ずメインスレッドで行うようにします:

Swift
// バックグラウンドでデータ更新 DispatchQueue.global(qos: .background).async { let realm = try! Realm() try! realm.write { // データ更新処理 } // UIの更新はメインスレッドで DispatchQueue.main.async { self.tableView.reloadData() } }

まとめ

本記事では、Realm SwiftでindexPath.rowを使ったデータ操作の実装方法を解説しました。

  • Resultsオブジェクトの特性を理解することで、安全にindexPath.rowでアクセスできる
  • オプショナルバインディングと適切なエラーハンドリングが重要
  • Realmの通知機能を使って、自動的にUIを更新できる

この記事を通して、Realmデータベースを使ったiOSアプリ開発で、リスト表示とデータ操作がスムーズに行えるようになりました。特に、Index out of boundsエラーを避けるためのベストプラクティスを身につけたことで、安定した動作するアプリを実装できます。

今後は、Realmのマイグレーションや、より複雑なクエリを使ったデータフィルタリングについても記事にする予定です。

参考資料