【Swift UI】Core Dataの使い方!SQLiteにデータを永続的に保存する
この記事からわかること
- Swift UIでCore Dataを利用する方法
- SQLiteの使い方
- .xcdatamodelファイルの役割と定義方法
- NSPersistentContainerクラスとは?
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.0
- Swift:5.9
- macOS:Sonoma 14.1
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
Core Dataとは?
Core DataとはAppleが提供しているデータを永続的に保存、管理するためのフレームワークです。アプリ内のモデルオブジェクト(データ)をRDB(リレーショナルデータベース)で管理できる形式に変換してくれる役割があります。データベース自体の構築や付随するCRUD処理などもCore DataのAPIとして提供されており、またデータベースはデバイス内に保管されるためアプリが停止した際にもデータを保持し、再度アプリが起動した際にデータを利用できるようになります。
Core DataはバックエンドストアとしてSQLiteが活用されていますが、保存方法はカスタマイズ可能でメモリやバイナリファイルへ保存先を変更することも可能です。メモリに保存する場合はアプリ起動中のみ保持される一時的なデータになるので永続的にデータを保持したい場合はデフォルト設定のSQLiteなどを使用します。
Core Dataとは?〜まとめ〜
- データを永続的に保存/管理するためのフレームワーク
- オブジェクトとデータベースのマッピング
- アプリを停止してもデータが残る
- SQLiteが使われている
- メモリに保存先を変更することも可能
NSManagedObjectContext
Core Dataの肝となるのがNSManagedObjectContext
の存在です。NSManagedObjectContext
はCRUD処理を行うための処理を主に提供していますが、実際にデータが永続化されるのはNSManagedObjectContext.save
メソッドを呼び出したタイミングです。
NSManagedObjectContext
はデータの状態変化を一時的にプールして管理している場所であり、NSManagedObjectContext.save
メソッドを呼ばないとNSManagedObjectContext
に蓄積したデータの変更永続化されないのでアプリを落として起動し直すと前の状態のままになってしまうので注意してください。
データを永続的に保存する他の方法
Swiftでアプリ内でデータを永続的に保存する方法は他にも存在します。
- テキストファイル
- UserDefaults
- Realm Swift(ライブラリ)
- Keychain
それぞれに特徴やメリットがありますが、Core Dataでは「データモデル定義の容易さとリレーションシップ」が大きなメリットだと思います。「Realm Swift」でも同じようなメリットがありますが、Core DataはApple公式のフレームワークであり、導入作業やバージョン管理が必要ないのも使い分けのポイントになると思います。
おすすめ記事
【Swift】FileManagerでファイルを保存!操作方法や格納場所
【Swift】UserDefaultsの使い方とは?データの保存/削除/取得/更新方法
Core Dataの特徴や用語集
- ・NSPersistentContainer・・・Core Dataを扱う上で欠かせない基幹クラス
- ・NSManagedObjectContext・・・データのCRUD処理を行うためのクラス。NSPersistentContainerから取得できる
- ・Entity(エンティティー)・・・データモデルのこと。「.xcdatamodeldファイル」に定義する。DB構造でいうところのテーブルを指し、行(実際のデータ)は「管理オブジェクト」と呼ぶ
- ・NSManagedObject・・・Entityが継承する基本クラス
- ・NSEntityDescription・・・Entityの詳細情報を表すクラス
Swift UIでの使い方
Swift UIでCore Dataを使用するためにはプロジェクト作成時に「Use Core Data」にチェックを入れる必要があります。
チェックを入れた状態でプロジェクトを生成すると「プロジェクト名.xcdatamodel」と「Persistence.swift」が自動で生成されます。
プロジェクト名.xcdatamodel
「プロジェクト名.xcdatamodel」はCore Dataで利用するデータモデル(Entity)を定義するためのファイルです。エンティティーを追加するには下部にある「Add Entity」をクリックし、データに持たせたい属性(Attributes)を追加していくだけです。データベースのテーブルを作っていくようなイメージですね。最初はサンプルで自動生成時にItem
が定義されています。
このファイルでエンティティー間のリレーションなども定義することが可能です。
Persistence.swift
「Persistence.swift」はCore Dataのロジック部分が記述されているファイルです。これもチェックを入れることで自動生成されます。コードの役割や意味をコメントで付与しておきました。
import CoreData
struct PersistenceController {
// シングルトン
static let shared = PersistenceController(inMemory: true)
// プレビュー用のデモデータ用(なくてもOK)
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
}()
let container: NSPersistentContainer
// 初期化&セットアップ処理」
init(inMemory: Bool = false) {
// .xcdatamodelファイル(モデルファイル名を渡す)
// 永続的なコンテナー インスタンスを作成
container = NSPersistentContainer(name: "CoreDataTest")
if inMemory {
// オンメモリで使用したい場合
// /dev/nullはLinuxにおいてゴミ箱的な役割の特別なファイル
print("InMemory")
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
} else {
// falseの場合は通常通りSQLiteファイルの保存する
print("InFile")
}
// 永続ストアをロード ストアが存在しない場合は自動作成
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// 親コンテキストからの変更を自動的にマージする
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
Swift UIでの実装
Swift UIでCore Dataを操作する場合は以下のようになります。これは「タイムスタンプをCore Data内で永続化して保存して表示するサンプルコード」です。
import SwiftUI
import CoreData
struct ContentView: View {
// コンテキストの取得
@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
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
// データの追加処理
/// エンティティをインスタンス化してデータを格納
/// viewContext.save() で反映
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
// データの削除処理
/// データを削除する
/// viewContext.save() で反映
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
マルチスレッドで扱うなら注意
Core DataはスレッドセーフではないのでContextを異なるスレッドから参照するとアプリがクラッシュしてしまう可能性があります。マルチスレッドで扱うためにはスレッドごとにContextを用意しておくのがベターなのでアプリの設計の段階でメインのみなのかバックグラウンドスレッドからも操作することがありえるのかを考慮しておく必要があります。
Core Dataを活用したデモプロジェクトも作成してみたのでよかったら参考にしてください。
おすすめ記事:MyCoreDataTest
Contextの変更を保存(永続化)する
Contextにデータの追加や更新、削除などをした場合はsave
メソッドを実行する必要があります。これでデータが永続化されます。hasChanges
メソッドでそもそもContext
に変更があったかどうかを識別することも可能です。またsave
メソッドはエラーを投げる可能性があるのでdo-catch
文でエラーを補足します。
/// 保存処理
private func saveContext(_ context: NSManagedObjectContext) {
guard context.hasChanges else { return }
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
エンティティクラスのインスタンス化の方法
エンティティクラス(NSManagedObject
)をインスタンス化するにはイニシャライザの引数にNSManagedObjectContext
を渡す必要があります。ここでContext
を渡すことで対象オブジェクトとContext
が紐づき、インスタンス化時にContext
に追加、プロパティ変更時にもContext
にその変更が反映されます。
/// 新規作成
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
エンティティクラスをインスタンス化する方法は他にいくつか存在します。その中にはContext
と紐付けない方法などもあるので詳細は以下の記事を参考にしてください。
データを追加する
先ほど紹介した方法ではインスタンスがContext
と紐づいていたので不要ですが、Context
と紐づいていないオブジェクトを生成した場合は明示的にContext
に追加する必要があります。追加はinsert
メソッドで行うことが可能です。
/// 追加処理
context.insert(object)
データを取得する
データベースからデータを取得するにはSwift UIの場合は@FetchRequest
を付与するだけで簡単に実装することが可能です。ソートやフィルタリングも行うことが可能です。
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults
Swift UIを使用したくない場合はNSFetchRequest
を使用します。
public func fetch() -> [Person] {
// String(describing: Person.self) → "Person" と同じ
let fetchRequest = NSFetchRequest<Person>(entityName: String(describing: Person.self))
do {
return try context.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return []
}
}
ジェネリックを使用して汎用的なfetchメソッドを実装することも可能なので詳細は以下の記事を参考にしてください。
データを削除する
データベースからデータを削除するにはdelete
メソッドを使用します。
context.delete(object)
保存先のファイルを指定する
Core Dataで保存先のファイルを変更するには保存したいデータベースのURLを作成し、NSPersistentStoreDescription(url:)
の引数に渡してpersistentStoreDescriptions
プロパティに渡してロードするだけです。
// 新しいデータベースのURLを指定
let newStoreURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("データベース名.sqlite")
// NSPersistentStoreDescriptionを作成し、URLを設定
let storeDescription = NSPersistentStoreDescription(url: newStoreURL)
// NSPersistentContainerを作成し、NSPersistentStoreDescriptionをセット
let container = NSPersistentContainer(name: "LinkMark")
container.persistentStoreDescriptions = [storeDescription]
// コンテナをロードします
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
ご覧いただきありがとうございました。