SwiftUIでのCore Dataの使い方を、標準テンプレートを読み解きながら解説します。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.3.2
【iOS】14.3
【macOS】Big Sur バージョン 11.1
標準テンプレートの生成
Xcodeで新規プロジェクトを生成します。
InterfaceはSwiftUIを選択
Life CycleはSwiftUI Appを選択
Use Core Dataにチェックを入れて下さい。
標準でCore Dataを利用したアプリのテンプレートコードが生成されます。
標準テンプレートを動かす
生成されたテンプレートをシミュレータで実行しても、ただ真っ白の画面が表示されるだけで何もできません。
テンプレートのコードが中途半端なのが理由なのですが、なぜAppleがそれを放置しているのかは不明です。
まずはContentViewのbody部を次のように修正し下さい。
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 |
var body: some View { NavigationView { // ナビゲーションバーを表示する為に必要 List { ForEach(items) { item in Text("Item at \(item.timestamp!, formatter: itemFormatter)") } .onDelete(perform: deleteItems) } .toolbar { /// ナビゲーションバーの左にEditボタンを配置 ToolbarItem(placement: .navigationBarLeading) { EditButton() } /// ナビゲーションバーの右に+ボタン配置 ToolbarItem(placement: .navigationBarTrailing) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } |
修正後シミュレータを実行すると、Editボタンと+ボタンが画面上に追加され、+ボタンでリストの行追加、Editボタンでリストの編集(削除のみ)が可能になります。
一旦アプリを終了してから再度立ち上げても、リストには前の状況が残っており、永続的なデータ保存が実現が確認できます。
次章以降で、標準テンプレートの詳細について解説します。
データモデルの定義
Core Dataで扱うデータモデルは、プロジェクト名.xcdatamodeldファイルで定義されています。
下図の「エンティティ名」がDBのテーブル名、「属性情報」がテーブルのカラム情報に相当します。
標準テンプレートでは、属性項目timestampを1つだけ持つ、 Itemテーブルが定義されています。
このデータモデル定義に従って、Itemクラスが生成されます。
Itemクラスはテーブルの1レコードを格納するクラスで、エンティティの基本クラスである被管理オブジェクト(NSManagedObject)を継承しています。
デフォルトの設定では、ItemクラスのソースコードはXcode上見えません。
リストの表示処理
データベースからデータを取得して、リストに表示する処理は、ContentViewの前半に記述されています。
わかりやすいようにコメントを追加しました。
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 |
struct ContentView: View { /// 被管理オブジェクトコンテキスト(ManagedObjectContext)の取得 @Environment(\.managedObjectContext) private var viewContext /// データベースよりデータを取得 @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], animation: .default) private var items: FetchedResults<Item> var body: some View { NavigationView { // ナビゲーションバーを表示する為に必要 /// 取得したデータをリスト表示 List { ForEach(items) { item in Text("Item at \(item.timestamp!, formatter: itemFormatter)") } .onDelete(perform: deleteItems) } /// ナビゲーションバーの設定 .toolbar { /// ナビゲーションバーの左にEditボタンを配置 ToolbarItem(placement: .navigationBarLeading) { EditButton() } /// ナビゲーションバーの右に+ボタン配置 ToolbarItem(placement: .navigationBarTrailing) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } |
被管理オブジェクトコンテキスト(ManagedObjectContext)の取得
1 2 3 |
@Environment(\.managedObjectContext) private var viewContext |
被管理オブジェクトコンテキスト(ManagedObjectContext)はデータの生成、保存、取得といったデータベース操作に必要な操作を行う為のオブジェクトです。
このオブジェクトを使ってデータベースアクセスする全ての操作を行います。
ManagedObjectContextはアプリ起動時に生成され、環境変数managedObjectContextに登録されています。
データベースよりデータを取得(Fetch処理)
1 2 3 4 5 6 |
@FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], animation: .default) private var items: FetchedResults<Item> |
プロパティラッパーの@FetchRequestを使ってデータベースを検索し、対象データ群をitemsプロパティに格納しています。
引数(sortDescriptors)で検索結果のソート条件(Itemテーブルのtimestamp属性が昇順)を指定、
引数(animation)で取得した検索結果の変更時に使用されるアニメーションタイプを.defaultに指定しています。
取得結果が格納されるitemsは被管理オブジェクト(ManagedObject)のコレクションであるFetchedResults<Item>型です。
ManagedObjectはテーブルの1レコードに該当するエンティティを保持するクラスで、ObservableObjectに準拠している為、値の変更がSwiftUIのViewと同期します。
取得したデータをリスト表示
1 2 3 4 5 6 7 8 |
List { ForEach(items) { item in Text("Item at \(item.timestamp!, formatter: itemFormatter)") } .onDelete(perform: deleteItems) } |
検索結果itemsの各レコードを、TextビューとしてListに表示しています。
item.timestampの出力形式を、formatterで指定していますが、これはSwiftUIで新たに追加されたLocalizedStringKey型で使えるようになった記述方法です。
なお、出力形式を示すitemFormatterは同じファイル内で次のように定義されています。
1 2 3 4 5 6 7 8 |
private let itemFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .medium return formatter }() |
.onDelete()はForEachのModifierで、コレクション(配列)から要素を削除する時に呼び出す処理を指定します。
ナビゲーションバーの設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.toolbar { /// ナビゲーションバーの左にEditボタンを配置 ToolbarItem(placement: .navigationBarLeading) { EditButton() } /// ナビゲーションバーの右に+ボタン配置 ToolbarItem(placement: .navigationBarTrailing) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } |
ナビゲーションバーの左側にEditボタン、右側に+ボタンを設定しています。
+ボタンが押された場合は、次章の項目追加処理(addItem)が呼ばれます。
項目追加処理
addItem()ファンクションで+ボタンを押された時に呼ばれる項目追加処理を行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private func addItem() { withAnimation { /// 新規レコードの作成 let newItem = Item(context: viewContext) newItem.timestamp = Date() /// データベースの保存 do { try viewContext.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } } |
新規レコードの作成
1 2 3 4 |
let newItem = Item(context: viewContext) newItem.timestamp = Date() |
ItemクラスのインスタンスであるnewItemを生成し、属性timestampに現在時刻(Date())を設定しています。
Itemクラスのイニシャライザには、引数としてデータベースを操作するManagedObjectContextを渡す必要があります。
データベースの保存
1 2 3 |
try viewContext.save() |
ManagedObjectContextのsave()メソッドで、データベースを保存(コミット)します。
このメソッドはエラーを返す可能性がある為、tryを使ったエラーキャッチが必要です。
アニメーションの指定
1 2 3 |
withAnimation { |
新規レコード追加表示時にアニメーションを適用しています。
@FetchRequestで取得したデータ群に対して、既にデフォルトのアニメーションタイプが指定されているので、この記述は蛇足のように思います。
項目削除処理
行の削除が要求された場合は、deleteItems()ファンクションが呼ばれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private func deleteItems(offsets: IndexSet) { withAnimation { /// レコードの削除 offsets.map { items[$0] }.forEach(viewContext.delete) /// データベースの保存 do { try viewContext.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } } |
レコードの削除
1 2 3 |
offsets.map { items[$0] }.forEach(viewContext.delete) |
記述が複雑で、ぱっと見わかりずいらいですが、同様の処理は次のように書き直せます。
1 2 3 4 5 |
for index in offsets { viewContext.delete(items[index]) } |
offsetsには削除対象の要素番号のコレクションが渡ってきますので、各要素番号に対応したエンティティをループで回して削除します。
削除には対象のエンティティを引数に、ManagedObjectContextのdelete()メソッドを呼び出します。
項目追加処理と同様に、データベースを保存(コミット)が必要です。
アプリ起動時の処理
新規プロジェクト作成時に生成される「プロジェクト名.swift」ファイル内に、アプリ起動時の処理が記述されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import SwiftUI @main struct SampleProjectApp: App { /// 永続コンテナのコントローラー生成 let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView() /// ManagedObjectContextを環境変数に追加 .environment(\.managedObjectContext, persistenceController.container.viewContext) } } } |
永続コンテナのコントローラー生成
1 2 3 |
let persistenceController = PersistenceController.shared |
Core Dataに必要なオブジェクトの作成と管理を行う永続コンテナ(PersistentContainer)のコントローラーを生成します。
コントローラーの詳細については後述します。
ManagedObjectContextを環境変数に追加
1 2 3 |
.environment(\.managedObjectContext, persistenceController.container.viewContext) |
データベース操作に使うManagedObjectContextを環境変数managedObjectContextに設定し、アプリケーションの各Viewで使用可能にします。
永続コンテナコントローラー(PersistenceController)
Core Dataに必要なオブジェクトの作成と管理を行う永続コンテナ(PersistentContainer)のコントローラーで、新規プロジェクト作成時にUse Core Dataチェックを入れると生成されるPersistent.swiftファイルに定義されています。
インスタンスの取得方法には次の2通りあります。
PersistenceController.shared
通常版の永続コンテナコントローラーを返します。
PersistenceController.preview
preview用の永続コンテナコントローラーを返します。
通常版との違いは次の2点です。
- preview用に初期データが設定される
- DBが実ファイルでなくメモリ上に構築される(コミットしても永続化されない)
プレビュー(preview)処理
1 2 3 4 5 6 7 8 |
struct ContentView_Previews: PreviewProvider { static var previews: some View { /// ManagedObjectContextを環境変数に設定 ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) } } |
ContentViewを表示するには、環境変数managedObjectContextが設定されている必要がある為、.environmentモディファイアを使って設定しています。
このとき使用しているPersistenceController.previewではプレビュー用のDB初期値が設定されます。
DB初期値は、Persistent.swiftの次の箇所で設定されていますので、ここを書き換えるとプレビュー時の初期値を変更可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static var preview: PersistenceController = { let result = PersistenceController(inMemory: true) let viewContext = result.container.viewContext /// プレビュー用初期値の設定 for _ in 0..<10 { let newItem = Item(context: viewContext) newItem.timestamp = Date() } do { try viewContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } return result }() |
合わせて読みたい記事






