UIKit × Core Dataで、基本的なCRUD操作を行うTODOアプリを実装したので、記録を兼ねた記事を書きました。
成果物
テーブルViewを使用したシンプルなTODOリストを実装しました。
以下GitHubリポジトリです。
環境
- Xcode15
- Swift 5.9
- iOS 17
Core Data概要
Core DataはAppleの全プラットフォームで使用することのできる、データ永続化(※0)フレームワークです。
アプリケーションの永続的なデータをオフラインで使用できるように保存したり、一時的なデータをキャッシュしたり、アンドゥ機能(※1)の追加をサポートします。さらに、iCloudアカウントを介して複数のデバイス間でデータを同期する機能も提供されています。
永続性:
オブジェクトをデータストアにマッピングする詳細を抽象化する。Core Dataは、UIとデータストアの間で、インターフェースの役割を果たすことができます。複雑なデータベース操作の知識がなくても、アプリケーションのデータを管理することを可能にしています。
アンドゥとリドゥ機能:
アプリケーション内で行われた個々の変更や一括処理された変更を追跡し、それらをロールバックすることができます。
バックグラウンドデータタスク:
バックグラウンドでデータ処理のタスクを実行することができます。
例えば、Webサービスからデータを受取り、それをオブジェクトにパースする作業をバックグラウンドで行い、結果をキャッシュすることができます。
ビューとの同期:
View synchronizationと呼ばれる、アプリケーションのUIと表示されているデータが常に同期されている状態を実現する機能を持っています。
バージョニングとマイグレーション:
Core Dataには、データモデルのバージョン管理や、ユーザのデータ移行を行うメカニズムが含まれています。
※0) 永続化:
プログラムがデータを生成または取得した後、そのデータを永久に保存するプロセス。アプリケーションが終了したりデバイスが再起動されたりしても、データが失われることなく、後から再アクセスできるのは、データが永続化されているから。
※1) アンドゥ機能:
ユーザーが行った操作を取り消し、それ以前の状態に戻す機能を指す。
例えば、ユーザーが間違えた操作をした場合、元の状態に戻すことはアンドゥ機能により実現されます。
事前準備
プロジェクトにData Modelを追加する
最初にTask Enitityを設定します。
AttributeはTitleとisFinishの2つを設定しました。
NSManagedObjectContextを取得する
NSManagedObjectContextはCore Dataの作業領域です。
オブジェクトの作成、変更、保存を行います。
iOS10から導入された新しい機能であり、NSManagedObjectContextを作成することで、NSManagedObjectModel、NSPersistentStoreCoordinator、NSManagedObjectContextの3つの生成と管理を一括で行うことができます。
ここでは、最もベーシックな手法を用いてNSManagedObjectContextを設定しました。
UIApplication.shared.delegate
を使用してAppDelegate
インスタンスにアクセスし、そこに定義されているpersistentContainer
のviewContext
を取得します。
private func getContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
NSManagedObjectModel、NSPersistentStoreCoordinator、NSManagedObjectContextを逐一生成するパターンの実装がGitHub Gistにありました。内部の挙動に興味のある方は以下参照してください。
これで、最小限Core Dataを操作する準備が整いました。
CRUD操作の実装に着手しましょう。
Read
- エンティティのインスタンスを取得するためのフェッチリクエストを作成する。
- 必要に応じてフェッチリクエストの結果をソートする。
- フェッチリクエストを実行する。
NSManagedObjectContext
を取得し、取得するオブジェクトが属するメモリ領域とデータ操作を行うための作業域を準備します。NSFetchRequest
を用いて、 Task
エンティティからどのようなデータを取得するか定義を行います。ここでは、 Task
オブジェクトを降順に取得するリクエストを作成しています。
NSManagedObjectContext
をfetch
し、取得したデータをtasks
配列に格納します。
let context = getContext()
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
let sortDescription = NSSortDescriptor(key: "title", ascending: false)
// contextを使用して、fetchRequestに基づいてデータを取得し、tasks配列に格納する
fetchRequest.sortDescriptors = [sortDescription]
do {
tasks = try context.fetch(fetchRequest)
} catch let error as NSError {
print(error.localizedDescription)
}
Create
- Core Dataモデル内の操作対象エンティティの定義を取得する。
- 定義に基づいて、新規にManagedObjectを作成しコンテキストに挿入する。
- ManagedObjectを変更する。
- context.save() を呼び出して、変更したManagedObjectを保存する。
NSEntityDescriptionで、Core Dataモデル内の「Task」という名前のエンティティの定義を取得し、指定したコンテキスト(context
)内のデータモデルから検索しています。
これにより、Task
オブジェクトを作成するために必要なエンティティの構造(属性、リレーションシップなど)を取得しています。
次に、新しいTaskオブジェクトを作成し、データベースのコンテキストに挿入します。
ここでは、init(entity:insertInto:)
を用いて、新しいTask
オブジェクトを初期化しています。
タイトルと完了状態を設定したのち、context.save()
を呼び出して、新しいTask
オブジェクトの保存を実行します。
save()
メソッドは、現在の NSManagedObjectContext
に対して行われた変更を永続ストアに変更する責務を持っています。データの変更・削除の時にも使用される、重要な関数です。
// 新しいタスクを保存する
internal func saveTask(withTitle title: String) {
guard let entity = NSEntityDescription.entity(forEntityName: "Task", in: context) else { return }
let taskObject = Task(entity: entity, insertInto: context)
taskObject.title = title
taskObject.isFinish = false
do {
try context.save()
tasks.append(taskObject)
} catch let error as NSError {
print(error.localizedDescription)
}
}
関係する公式ドキュメントです。
context.save()
context.save()
はNSError
オブジェクトへのポインタをパラメータとして渡されており、エラーが発生した場合に詳細を出力することができます。
hasChanges()
hasChanges()
は、コンテキストに未コミットの変更があるかどうかを示すBool値です。
Core Dataが不必要な作業を行う可能性を防ぐため、saveメソッドを呼び出す前に、必ず実行する旨が公式ドキュメントに記述されています。
Update
- NSFetchRequestを使用して、変更する エンティティをコンテキストから検索する。
- 検索で見つかった 操作対象エンティティのプロパティを更新する。
- コンテキストに未コミットの変更がある場合、context.save() を呼び出して変更を永続ストアに保存する。
NSFetchRequest
を用いて、特定のエンティティ(この記事では Task
エンティティ)からどのようなデータを取得するか定義を行います。ここでは、タイトルが同じ Task
オブジェクトを取得するリクエストを作成しています。
NSManagedObjectContext
を使用して、NSFetchRequest
で定義されたリクエストに基づいてデータのフェッチを行います。フェッチされたデータは、必要に応じて更新されます。
// タスクのステータスを更新する
internal func changeDone(at indexPath: Int) {
guard let title = tasks[indexPath].title else {
print("指定されたタスクにタイトルがない")
return
}
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
// フェッチリクエストの結果をフィルタリング
fetchRequest.predicate = NSPredicate(format: "title == %@", title)
do {
let results = try context.fetch(fetchRequest)
guard let taskToUpdate = results.first else {
print("タイトルが一致するタスクが見つかりません")
return
}
taskToUpdate.isFinish = !taskToUpdate.isFinish
if context.hasChanges {
try context.save()
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
Delete
- 削除するエンティティをコンテキストから検索する。
- エンティティの削除を行う。
- 変更内容を保存する。
delete()
メソッドを通じ、指定されたオブジェクトは、変更がコミットされるときにその永続ストアから削除されるべきだと明示されます。
コンテキストに対してsave()
メソッドを呼び出し、削除を永続ストアへ反映します。
// タスクの削除を行う
internal func deleteTask(at indexPath: Int) {
guard let title = tasks[indexPath].title else {
print("指定されたタスクにタイトルがない")
return
}
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
// フェッチリクエストの結果をフィルタリング
fetchRequest.predicate = NSPredicate(format: "title == %@", title)
do {
let results = try context.fetch(fetchRequest)
guard let taskToDelete = results.first else {
print("タイトルが一致するタスクが見つかりません")
return
}
context.delete(taskToDelete)
// コンテキストに変更がある場合のみ保存を試みる
if context.hasChanges {
try context.save() // 変更を保存
tasks.remove(at: indexPath) // 配列からもタスクを削除
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
まとめ
Core Dataの概要と、主要な機能。CRUD操作の具体的な実装方法を説明しました。
本稿がCore Dataを理解する上で一助となれば幸いです。ここまで読んでくださりありがとうございました。