SwiftUIでCore Dataの基本的な使い方を簡単なTodoリストアプリを例に解説します。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.3.2
【iOS】14.4
【macOS】Big Sur バージョン 11.1
新規プロジェクトの生成
Core Dataを使った新規プロジェクトの作成方法は、【SwiftUI】Core Dataの使い方:準備編を参照して下さい。
デフォルトで生成される標準テンプレートのコードとエンティティ(Item)は削除してしまってOKです。
Persistence.swift のpreview初期データ設定部分も忘れずに削除して下さい。
データモデルの定義
Todoリストアプリに必要なデータモデルを定義します。
Model Editor を開いて、次の Task エンティティを作成します。
属性項目は次の3つです。
timestamp : タスクの作成日付(Date型)
checked:タスクがチェック済かどうかのフラグ(Boolean型)
name:タスク名(String型)
Model Editorの使い方は【SwiftUI】Core Dataの使い方:エンティエィ(Entity)を定義するで解説しています。
Todoリストアプリの全コード
以下がTodoリストアプリの全コード(ContentView.swift)です。
Core Dataの操作に関する主要部分については、以降の節で解説します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
import SwiftUI import CoreData struct ContentView: View { /// 被管理オブジェクトコンテキスト(ManagedObjectContext)の取得 @Environment(\.managedObjectContext) private var context /// データ取得処理 @FetchRequest( entity: Task.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Task.timestamp, ascending: true)], predicate: nil ) private var tasks: FetchedResults<Task> var body: some View { NavigationView { /// 取得したデータをリスト表示 List { ForEach(tasks) { task in /// タスクの表示 HStack { Image(systemName: task.checked ? "checkmark.circle.fill" : "circle") Text("\(task.name!)") Spacer() } /// タスクをタップでcheckedフラグを変更する .contentShape(Rectangle()) .onTapGesture { task.checked.toggle() try? context.save() } } .onDelete(perform: deleteTasks) } .navigationTitle("Todoリスト") /// ツールバーの設定 .toolbar { ToolbarItem(placement: .navigationBarLeading) { EditButton() } ToolbarItem(placement: .navigationBarTrailing) { NavigationLink(destination: AddTaskView()) { Image(systemName: "plus") } } } } } /// タスクの削除 /// - Parameter offsets: 要素番号のコレクション func deleteTasks(offsets: IndexSet) { for index in offsets { context.delete(tasks[index]) } try? context.save() } } /// タスク追加View struct AddTaskView: View { @Environment(\.managedObjectContext) private var context @Environment(\.presentationMode) var presentationMode @State private var task = "" var body: some View { Form { Section() { TextField("タスクを入力", text: $task) } } .navigationTitle("タスク追加") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("保存") { /// タスク新規登録処理 let newTask = Task(context: context) newTask.timestamp = Date() newTask.checked = false newTask.name = task try? context.save() /// 現在のViewを閉じる presentationMode.wrappedValue.dismiss() } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) } } |
データ取得処理
SwiftUIではデータベースの検索結果とViewを同期する為の大変便利な仕組みとしてプロパティラッパー(@FetchRequest)が用意されています。
@FetchRequestを使ってプロパティを宣言すると、プロパティに検索結果が格納されるとともに、データの変更に応じて検索結果が常に最新に保たれます。
このプロパティを使ってViewを生成すると、データの変更がViewに即時反映される仕組みです。
1 2 3 4 5 6 7 8 |
/// データ取得処理 @FetchRequest( entity: Task.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Task.timestamp, ascending: true)], predicate: nil ) private var tasks: FetchedResults<Task> |
@FetchRequest(…) の部分がプロパティラッパーの定義、private var tasks… 以降がプロパティの宣言です。
@FetchRequestには、検索対象エンティティ(entity:)、ソート順(sortDescriptors:)、抽出条件(predicate:)などの引数が指定可能です。
検索結果が配置されるプロパティは FetchedResults<エンティティクラス> のコレクション型で、1レコードに該当するエンティティクラス(NSManagedObjectの派生クラス)の配列を保持します。
サンプルコードでは、Task エンティティを対象とし、抽出条件無し(=全て)、timestamp 属性の昇順でソートした検索結果を、プロパティ tasks に配置しています。
新規登録処理
データの新規登録は、エンティティクラス(NSManagedObjectの派生クラス)のインスタンスを生成して実現します。
インスタンス生成時の引数contextにはNSManagedObjectContextを指定します。
1 2 3 4 5 6 7 |
/// タスク新規登録処理 let newTask = Task(context: context) newTask.timestamp = Date() newTask.checked = false newTask.name = task |
サンプルコードではエンティティクラスTaskのインスタンスを生成後、初期属性値を設定しています。
これで、見かけ上は新たなレコードが追加されたような振る舞いをしますが、あくまでNSManagedObjectContext内にインスタンスが追加されただけにすぎません。追加したレコードをアプリが終了しても消えないよう永続化する為には、NSManagedObjectContextの変更をデータベースファイルに保存する必要があります。
RDBで言う、コミット処理です。
1 2 3 |
try? context.save() |
保存処理にはNSManagedObjectContextの save() メソッドを使います。
この処理はエラーを返す可能性があるためtryをつけて呼び出します。
サンプルコードでは try? でエラーを無視していますが、必要に応じてエラー処理を追記してください。
変更処理
変更処理はエンティティインスタンスの属性を変更するだけです。
変更を永続化する為には、保存処理も忘れないようにしましょう。
1 2 3 4 5 6 7 8 |
/// タスクをタップでcheckedフラグを変更する .contentShape(Rectangle()) .onTapGesture { task.checked.toggle() try? context.save() } |
サンプルコードでは、タスクセルをタップした時に、checked フラグを切り替えています。
削除処理
削除処理にはNSManagedObjectContextの delete() メソッドを使います。
引数には削除するエンティティのインスタンスを指定します。
追加や変更時と同様に、削除後は保存処理を呼び出して下さい。
1 2 3 4 5 6 7 8 9 10 |
/// タスクの削除 /// - Parameter offsets: 要素番号のコレクション func deleteTasks(offsets: IndexSet) { for index in offsets { context.delete(tasks[index]) } try? context.save() } |
サンプルコードでは、セルの削除操作が行われた時にこの処理が呼び出され、対象のインスタンスを削除しています。
条件付き保存処理
保存処理に使うNSManagedObjectContextの save() メソッドですが、一般的にはデータの挿入や削除など具体的な変更を行った後に呼ばれます。
しかし、すべての変更を施した後に一括で保存するようなケースもあります。
その場合は、save() を呼び出す前に、保存されていない変更が無いかチェックして必要のない作業をさせないようにするのが良いでしょう。
NSManagedObjectContextには hasChanges プロパティがあり、コンテキストが所有するオブジェクトに変更があるかどうかチェック可能です。
次のように使います。
1 2 3 4 5 |
if context.hasChanges { try? context.save() } |
サンプルコードでpreviewが動かない
本サンプルコードはシミュレーターや実機では動くのですが、なぜか preview で実行しようとすると crash して動きません。
これは初期データが無い状態で、対象テーブルを Fetch しようした時に発生します。(Xcodeの不具合だと思うのですが・・)
これは、preview用の初期データを用意すると回避できます。
Persistence.swift の次の箇所に preview用初期データ登録処理を追加し、cmd+B で再Buildして下さい。
resumeでは無くて再Buildです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct PersistenceController { static let shared = PersistenceController() static var preview: PersistenceController = { let result = PersistenceController(inMemory: true) let viewContext = result.container.viewContext /// preview用初期データ登録処理 let newTask = Task(context: viewContext) newTask.timestamp = Date() newTask.checked = false newTask.name = "初期タスク" do { try viewContext.save() |