はじめに
ネットで CoreData について色々調べてみた際、現状、CoreData に関する情報が色んなサイトや記事に散逸していたため、この記事で自分なりにまとめ、備忘録として残すことにしました。
これから CoreData について調べる多くの方々が、まずは最初にこの記事を見て確認いただけるようなものを目指して書いたつもりです。手順は極力簡素化しています。
また、この記事の情報は2023年10月26日のものであり、今後の Xcode のアップデートなどにより実際とは異なる情報になる可能性があります。
追加して欲しい内容などございましたら、お気兼ねなくコメントを残していただくか、X(Twitter)などにご連絡ください。
使用環境
Swift 5
Xcode: Version 15.0 (15A240d)
永続保存の種類
- CoreData
- Swift でアプリのデータを保存する場合は基本的にこちらを使用。フィルタやソートの機能などが充実している。
- 今回はこちらをメインに紹介します。
- UserDefaults
- 1行で読み込み、書き込みができ、クラウドにも保存が可能。しかし、ソートやフィルタができず、また容量制限がある(らしい)
- その容量制限の関係上、大量のデータを保存してしまうのはよろしくなさそうなので、その場合は CoreData を使用することを推奨。
※ 他にも方法として Key Chain、CloudKit などがありますがここでは割愛します。
【参考】iOSにユーザーデータを保存する方法と、そのためのコードの書き方: UserDefaults、Core Data、Key Chain、CloudKit
CoreDataの導入(簡易版)
- 新規でプロジェクトを作成する場合
- project の作成時、Choose options for your new project: の Storage で「Core Data」を選択。
-> project 作成後、AppDelegate.swift に CoreData に関するコードが作成されます。 - 「Host in CloudKit」にチェックを入れる。
※ NSPersistentCloudKitContainer が NSPersistentContainer の機能を兼ねるため、基本的にはチェック推奨。
- project の作成時、Choose options for your new project: の Storage で「Core Data」を選択。
- 既存プロジェクトに導入する場合
- Xcode のウィンドウの左下の「+」をクリックし、「New File...」を選択。
- Choose a template for your new file: で「Data Model」を選択。(フィルタを使うと楽です。)
- ファイル名は(特別な理由がない場合)アプリ名を入力。
- AppDelegate.swift の上部に
import CoreData
を記入。 - AppDelegate.swift に 以下のコードを記入。(TestCoreData の部分はアプリ名に置き換えてください)
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentCloudKitContainer(name: "TestCoreData")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.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)")
}
}
}
※ なお、新規でプロジェクトを作成し、「Host in CloudKit」にチェックを入れた場合のみ、自動で 『<プロジェクト名>.entitlements』というファイルが作成されます。これは
entitlementsとはプッシュ通知やApp Groupsなど、プロジェクトのCapabilityでの設定に関するファイルです。
Capabilityで通知の追加やApp Groupsのcontainer追加時に自動更新されます。
【参考】Xcodeの[プロジェクト名].entitlementsというファイルについて調べてみる
というものだそうです。もしこれが必要となる場合、別途新規でプロジェクトを作成し、ファイルを使用するプロジェクトのフォルダ内に入れれば使えるかもしれません。(未検証)
※ もし既存のプロジェクトへの導入時、エラーが発生したりうまく動作しなかった場合、新しく CloudKit を導入したプロジェクトを作成し、そこからコードやファイルの違いを比較して追加や導入を行なってみてください。
CoreData で出てくる用語集
CoreData に出てくる用語はやたらと長く、覚えるのも大変なので、参照する場合は以下のリストから検索すると便利かと思います。
かなり簡略化して書いてます。もし間違いや、追加して欲しい用語などございましたら教えてください。
-
NSPersistentContainer / NSPersistentCloudKitContainer
- CoreData を扱うための機能が全部入ったクラス。
-
NSManagedObjectContext
- エンティティモデルの NSManagedObject の insert(作成)、select(検索)、update(変更)、delete(削除) を可能とするスペース。
-
NSEntityDescription
- 「エンティティのモデルクラス」の定義と「インスタンス」を管理するクラス。
- NSManagedObject インスタンスを作成する場合は基本的にこれを使う。
-
NSManagedObject インスタンス
- NSManagedObjectContext を経由して取得されるインスタンス。
- これや、これのプロパティに対して操作をすることで、データの変更や削除が可能。
-
NSFetchRequest
- 『SQLのSELECT文』に該当し、NSManagedObject インスタンスを呼んでくる際に使用する。
- NSPredicate や NSSortDescriptor を併用することでフィルタやソートが可能。
-
NSPredicate
- 『SQLのWHERE句』に該当し、検索フィルタに使用できる。
- 配列で複数のフィルタを設定可能。
-
NSSortDescriptor
- 『SQLのORDER BY句』に該当し、オブジェクトに共通のプロパティに従ってオブジェクトの並びを順序付ける。
-
.xcdatamodeld
- テーブルを定義する。
【参考】SwiftでCoreData
【参考】Core DataをSwift 4で使う (iOS 10以降)
【参考】Core Dataについて調べたメモ
CoreDataの基本的な使い方
先に参考にさせていただいた記事を紹介しておきます。こちらの記事に使用方法は詳しく書かれておりますので、お手数ですが先にご確認ください。
以下ではこの情報に基づき、ポイントを書いていきます。
- 上記サイトの「保存ロジックを書く」で、saveContext() に改良を加えられていますが、これはバックグラウンドコンテキストでも使えるように直しているようです。(バックグラウンドで CoreData を使用するための手順は 【Swift】Core Dataをバックグラウンドで使う を参照してください。この内容については、当記事の『CoreDataに関する情報+α』で少し触れます。)
- 上記サイトでは、Person と Career の2つのエンティティを作成し、それぞれを親クラスと子クラスとして取り扱っています。
- 一般的に、CoreData の使用手順を紹介する多くのサイトで、画面右ペインの「Codegen」を「Manula/None」に変更して使用しています。こうすることで「[Entity]+CoreDataClass.swift」や「[Entity]+CoreDataProperties.swift」などのファイルでプロパティ、またRelationShip(親クラスと子クラスを繋げる手続き)の状態で使用できるメソッドが可視化されたり、ファイルに追記することでカスタマイズが可能になります。
++ カスタマイズの例 ++
nil結合演算子(??)を使用することで、意図せずにプロパティに nil が入るようなトラブルを防ぐことができる。
@NSManaged public var name: String?
-> public var wrappedName: String { name ?? "" }
【参考】【SwiftUI】Core Dataの使い方:エンティエィ(Entity)を定義する
- 上記サイトのメソッドを私が独自に改良したメソッドを以下に紹介します。(NSPredicate や NSSortDescriptor についてはこの記事の『NSPredicate / NSSortDescriptor の使い方』で紹介します。)
「フィルタ」「ソート」「ソートの昇順/降順」を一度に指定するメソッド(親なし)
// 【TestEntity】データを読み込む(複数条件でソート、フィルタが可能)
static func getTestEntities(predicates: [NSPredicate]/* Empty だと無効 */,
sortKeys: [String]/* Empty だと無効。ソートの対象となるプロパティを String で指定する。 */,
sortAscendings: [Bool]/* sortKeys と配列番号が対応。Empty だと無効。true で昇順 */) -> [TestEntity] {//MARK: 【TestEntity】データを読み込む(複数条件でソート、フィルタが可能)
/* Predicate・sortDescriptors を全て含んだメソッド */
let context = persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TestEntity")
/* 全てのフィルタの条件をAND条件でフィルタリング */
fetchRequest.predicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
/* 複数のソート条件を追加するための配列 */
var sortDescriptor_Array: [NSSortDescriptor] = []
/* ソートのルールを適用する */
if !sortKeys.isEmpty{
for i in 0..<sortKeys.count{
sortDescriptor_Array.append(NSSortDescriptor(key: sortKeys[i], ascending: sortAscendings[i]))
}
// 全てのソート条件を適用する
fetchRequest.sortDescriptors = sortDescriptor_Array
}
do {
let TestEntities = try context.fetch(fetchRequest) as! [TestEntity]
return TestEntities
}
catch {
fatalError()
}
}
「フィルタ」「ソート」「ソートの昇順/降順」を一度に指定するメソッド(親がいる可能性あり)
// 【Child】データを読み込む(親の指定や、複数条件でソート、フィルタが可能)
static func getChildren(parent: Parent?,/* 親を指定しない場合、nil を入力 */
predicates: [NSPredicate]/* Empty だと無効 */,
sortKeys: [String]/* Empty だと無効。ソートの対象となるプロパティを String で指定する。 */,
sortAscendings: [Bool]/* sortKeys と配列番号が対応。Empty だと無効。true で昇順 */) -> [Child] {//MARK: 【Child】データを読み込む(親の指定や、複数条件でソート、フィルタが可能)
/* Predicate(親含む)・sortDescriptors を全て含んだメソッド */
let context = persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Child")
/* 複数のフィルタ条件を追加するための配列 */
var predicates_Array: [NSPredicate] = []
/* 親で限定する */
if let parent = parent{
predicates_Array.append(NSPredicate(format: "parent == %@", parent)) // RelationShip にある属性情報
}
/* フィルタを追加する */
predicates_Array += predicates
/* これまでのフィルタの条件をAND条件でフィルタリング */
fetchRequest.predicate = NSCompoundPredicate(type: .and, subpredicates: predicates_Array)
/* 複数のソート条件を追加するための配列 */
var sortDescriptor_Array: [NSSortDescriptor] = []
/* ソートのルールを適用する */
if !sortKeys.isEmpty{
for i in 0..<sortKeys.count{
sortDescriptor_Array.append(NSSortDescriptor(key: sortKeys[i], ascending: sortAscendings[i]))
}
// 全てのソート条件を適用する
fetchRequest.sortDescriptors = sortDescriptor_Array
}
do {
let children = try context.fetch(fetchRequest) as! [Child]
return children
}
catch {
fatalError()
}
}
NSPredicate の使い方
CoreData のフィルタの条件として使用する NSPredicate において、よく使いそうなものや、すぐに思いつかなそうなものを以下に列挙してみました。
※ NSPredicate は、複数条件でフィルタする場合は、配列で指定できます。
// 例えば、以下のように変数を定義して、これに .append していく
var predicates_Array: [NSPredicate] = []
...
// フェッチ前にフィルタ条件を付加する
fetchRequest.predicate = NSCompoundPredicate(type: .and, subpredicates: predicates_Array)
- プロパティ attrText(String型)が nil である
predicates_Array.append(NSPredicate(format:"attrText == nil"))
- プロパティ attrText(String型)が nil ではない
predicates_Array.append(NSPredicate(format:"attrText != nil"))
- プロパティ attrText(String型)が "word" と一致する
predicates_Array.append(NSPredicate(format:"attrText == %@", "word"))
- プロパティ attrText(String型)が "word" と一致しない
predicates_Array.append(NSPredicate(format:"attrText != %@", "word"))
- プロパティ attrText(String型)が "word" を含む
predicates_Array.append(NSPredicate(format:"attrText CONTAINS[c] %@", "word"))
- プロパティ attrText(String型)が "word" を含まない
predicates_Array.append(NSPredicate(format:"NOT attrText CONTAINS[c] %@", "word"))
- プロパティ attrText(String型)が、strings_Array([String]型)の各要素のいずれかに一致する
predicates_Array.append(NSPredicate(format:"attrText IN %@", strings_Array))
- プロパティ attrText1(String型)が、strings1_Array([String]型)の各要素のいずれかに一致する
もしくは
プロパティ attrText2(String型)が、strings2_Array([String]型)の各要素のいずれかに一致する
のいずれかである。
predicates_Array.append(NSPredicate(format:"(attrText1 IN %@) OR (attrText2 IN %@)", strings1_Array, strings2_Array))
- プロパティ isOn(Bool型)が true である
predicates_Array.append(NSPredicate(format:"isOn = 1"))
- プロパティ isOn(Bool型)が false である
predicates_Array.append(NSPredicate(format:"isOn = 0"))
- プロパティ theNumber_Int16(Int16型)が theNumber と一致する
predicates_Array.append(NSPredicate(format:"theNumber_Int16 = %D", theNumber))
- プロパティ theNumber_Int16(Int16型)が theNumber と一致しない
predicates_Array.append(NSPredicate(format:"theNumber_Int16 != %D", theNumber))
- プロパティ theNumber_Int16(Int16型)が theNumber より大きい
predicates_Array.append(NSPredicate(format:"theNumber_Int16 >= %D", theNumber))
- プロパティ theNumber_Int16(Int16型)が theNumber より小さい
predicates_Array.append(NSPredicate(format:"theNumber_Int16 <= %D", theNumber))
- プロパティ createdDate(Date型) が theDate より後のもの
predicates_Array.append(NSPredicate(format: "createdDate >= %@", theDate as CVarArg))
- プロパティ createdDate(Date型) が theDate より前のもの
predicates_Array.append(NSPredicate(format: "createdDate <= %@", theDate as CVarArg))
- エンティティ(親)parent において、そのサブエンティティ(子)child のプロパティ name(String型)と word が一致するものが1つ以上存在する
predicates_Array.append(NSPredicate(format:"SUBQUERY(child, $c, $c.name == %@).@count > 0", word))
- エンティティ(親)parent において、そのサブエンティティ(子)child のプロパティ name(String型)と word が一致するものが1つもない
predicates_Array.append(NSPredicate(format:"SUBQUERY(child, $c, $c.name == %@).@count == 0", word))
- エンティティ(親)parent において、そのサブエンティティ(子)child のプロパティ name(String型)に word を含むものが1つ以上存在する
predicates_Array.append(NSPredicate(format:"SUBQUERY(child, $c, $c.name CONTAINS[c] %@).@count > 0", word))
- エンティティ(親)parent において、そのサブエンティティ(子)child のプロパティ name(String型)に word を含まないものが1つ以上存在する
predicates_Array.append(NSPredicate(format:"SUBQUERY(child, $c, NOT $c.name CONTAINS[c] %@).@count > 0", word))
- エンティティ(親)parent において、そのサブエンティティ(子)child のプロパティ name(String型)が strings1_Array([String]型)の各要素のいずれかに一致するものが1つ以上存在する
predicates_Array.append(NSPredicate(format:"SUBQUERY(child, $c, $c.name IN %@).@count > 0", strings1_Array))
その他、演算子、プレースホルダー、集計結果については下記サイトが参考になります。
NSSortDescriptor の使い方
CoreData の出力結果のソートに使用する NSSortDescriptor における基本的な使用法を紹介します。
※ NSSortDescriptor は、複数条件でソートする場合は、配列で指定できます。
// 例えば、以下のように変数を定義して、これに .append していく
var sortDescriptor_Array: [NSSortDescriptor] = []
...
/* key には「どのプロパティでソートするか」を String で、
ascending には「昇順、降順のどちらでソートするかを」Bool で指定(true が昇順)する。*/
sortDescriptor_Array.append(NSSortDescriptor(key: "attr", ascending: true /* 昇順 */))
...
// フェッチ前にソート条件を付加する
fetchRequest.sortDescriptors = sortDescriptor_Array
objectID について
CoreData で作成されるオブジェクトには、デフォルトで .objectID というプロパティが存在します。データ型は NSManagedObjectID であり、この内容はオブジェクト作成後に変更することができません。よって、マルチスレッドでデータにアクセスするような状況で利用できます。
printしてみると、以下のような出力が確認できます。
0x999eeff967095693 <x-coredata://2E47B111-8E69-4036-8C08-AABBCCE4F350/Person/p1>
ここで、最後尾に "p1" という箇所が確認できますが、これはこのエンティティの NSManagedObject インスタンスを作成した順番を示します。(p1, p2, p3,... のように増えていく)
また、例えばオブジェクトの任意の1つを削除した場合、その後でオブジェクトを作成したらこの番号はどうなるでしょうか。検証してみると欠けた番号は補完されず、新しい番号が割り当てられました。
もし、objectID の順にソートして読み込みたい場合、NSSortDescriptor を以下のように記述します。(Swift5の場合。Person はエンティティ名)
NSSortDescriptor(keyPath: \Person.objectID, ascending: true)
この書き方は、自分で設定した Attribute に対しても利用できます。そうすることでコンパイルチェックが可能となり、記述ミスを減らすことができます。
NSSortDescriptor(keyPath: \Person.name, ascending: true)
さらに、objectID でフィルタをしたい場合、以下のように記述します。(obj は NSManagedObject インスタンス)
NSPredicate(format: "self = %@", obj.objectID)
なお、例えば objectID の一意性を利用してそこから文字列を生成し、外部のDBのデータなどと紐づけをしたいといった場合、以下のコードで objectID から String を作ることができます。
obj.uriRepresentation().absoluteString
// x-coredata://2E47B111-8E69-4036-8C08-AABBCCE4F350/Person/p1
【参考】iOSアプリでデータベース(CoreData)を使う時に必要な知識をまとめてみた - Swift編 -
【参考】CoreDataでEntityのID順にソートしたデータを取得する方法
【参考】NSSortDescriptor get key from core data in Swift 4
フォールト について
プロパティにアクセスするまでは値を読み込まないことで、メモリを節約する機能です。
NSManagedObject インスタンスを読み込み、プロパティにアクセスしていないで print すると、以下のような出力が確認できます。
<Person: 0x282a554a0> (entity: Person; id: 0x97ccaa12dfeee141 <x-coredata://2E47B111-8E69-4036-8C08-AABBCCE4F350/Person/p1>; data: <fault>)
最後の fault は、まだプロパティを読み込んでいないことを示し、仮に1つでもプロパティにアクセスした後で再度 print を実行すると
<Person: 0x282a554a0> (entity: Person; id: 0x97ccaa12dfeee141 <x-coredata://2E47B111-8E69-4036-8C08-AABBCCE4F350/Person/p1>; data: {
name = "Test";
height = 100;
width = 120;
})
のように全てのプロパティが表示されるようになります。
【参考】iOSアプリでデータベース(CoreData)を使う時に必要な知識をまとめてみた - Swift編 -
有効範囲の設定
.xcdatamodeld ファイルを開き、Entity の Attribute を選択した状態で
Show the Data Model inspector > Attribute > Validation から値の最小値、最大値の設定が可能です。
String の場合は文字列の長さの範囲を指定できます。(Min Length, Max Length)
【参考】iOSアプリでデータベース(CoreData)を使う時に必要な知識をまとめてみた - Swift編 -
CoreData をバックグラウンドで使用する
以下のサイトで、CoreData をバックグラウンドで使用する方法について(非常に詳しく)紹介されています。
ただ、かなり内容が長かったため、以下では私個人がかいつまんで簡略化した内容を箇条書きで書いておきますので参考までに。
- バックグラウンドでの CoreData の使用に関する対処は、以下を実施した場合、UIの更新を止めてしまう可能性があり、それを避けるために必要。
- バックグラウンドタスクなどのサブスレッド上で動いているものから CoreData への書き込み
- 一度に時間のかかる大量のデータの処理
- CoreDataでは、複数のスレッドやキューをまたいで使用した際のデータの不整合やアプリのクラッシュを、シリアルキューを使用して回避するようにしている。しかしバックグラウンドで CoreData を使用する際は「その方法で回避できなくなる可能性があり」、管理オブジェクトはそのコンテキストが持つキューから呼ばれた処理の中でのみ登場するようにする必要がある。
- perform は DispatchQueue.async、performAndWait は DispatchQueue.sync のようなもの。
- DispatchQueue の sync はメインスレッドからメインキュー(DispatchQueue.mainとか)の sync を呼ぶとデッドロックを発生させるが、performAndWait はリエントラント(再入可能)となっているため、こちらはデッドロックになることなく処理が進められる。
- context.persistentStoreCoordinator で parent に設定したコンテキストがさらに親を持つような場合、最後の親(persistentStoreCoordinator が設定されたもの)にたどり着くまで保存を繰り返す必要がある。
- performBackgroundTask は「バックグラウンド」かつ「使い捨て」
- newBackgroundContext は「バックグラウンド」かつ「使い回し」
- NSManagedObjectContextのイニシャライザ は「メインスレッド(mainQueueConcurrencyType)」もしくは「親子関係のコンテキスト(privateQueueConcurrencyType)」で使用する
- perform メソッドは autorelease pool が内部で適用されており、かつクロージャーを抜けた後でメモリは解放され、自動的に processPendingChanges メソッドが実行される。
- processPendingChanges というのは、コンテキストのsaveの一歩手前、UNDOマネージャにそこまでの変更が登録されるメソッドである。
- for文などでサブルーチンを抜けるまでメモリの取得が数多く繰り返し行われた場合、その途中でメモリ不足となってしまうことがあるが、それを防ぐために for文の中を autoreleasepool { } で括ると、このスコープの中で作成したインスタンスはこのスコープを抜けた時に解放が行なわれる。
- performBackgroundTask は newBackgroundContext + perform のようなものだが、呼び出すたびに内部で新たなコンテキストが作成されるため、ネストして呼び出してもそれぞれ別物のコンテキストになるという違いがある。
- マージのポリシーは以下の4+1種類
- NSMergeByPropertyStoreTrumpMergePolicy(プロパティ単位でマージ/ストア優先)
- NSMergeByPropertyObjectTrumpMergePolicy(プロパティ単位でマージ/in-memory優先)
- NSOverwriteMergePolicy(管理オブジェクト単位でマージ/in-memory優先)
- NSRollbackMergePolicy(管理オブジェクト単位でマージ/ストア優先)
- NSErrorMergePolicy(例外エラーを発生)
※ NSErrorMergePolicy は、上記サイトの検証では NSMergeByPropertyObjectTrumpMergePolicy と同じ結果となっている。
- Xcodeのメニュー > Product > Scheme > Edit Scheme… > Run (Debug) > Arguments タブの「Arguments Passed On Launch」に以下の文字列を加えると、誤ったコンテキストや管理オブジェクトの使い方(スレッドやキュー間にまたがって使用した場合等)をするとその場所でブレイクしてくれる。
-com.apple.CoreData.ConcurrencyDebug 1
ダウンキャストについて
以下のサイト
において、 NSFetchRequest を使用して NSManagedObject インスタンスを読み込んでくる際の指摘として、以下のようなコード
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Company")
を、ダウンキャストが不要であるという観点から、
let request = NSFetchRequest<Company>(entityName: "Company")
のように訂正したほうが良いという情報もあります。ただ、ダウンキャストが何故ダメなのか?についての記述がなく(おそらくパフォーマンスが下がる?)、また通常であれば、メソッドをコピペして使用する方にとっては、修正箇所が増えてしまう煩わしさもあり、直したくないという方もいるかと思います(私もその一人)。
なので私個人としては現在もこの訂正をせずにフェッチをするようにしています。
最後に
iOS17 より使用可能となる SwiftData という、おそらく CoreData の後継にあたるフレームワークが WWDC2023 にて発表されました。まだ新しく生まれたフレームワークですが、どうやらコードだけで設定や処理が完結するものらしく、かなり使い勝手が良さそうです。
ただし、まだ枯れていないこともあり、「やっぱりまだ CoreData で良いや〜」となる可能性も捨てきれません。(仮にそうなったとしても、多分これからどんどん使いやすくなると思います)
そういったことや、iOS17 からしか使えないことなどを考慮すると、現状では SwiftData と CoreData の情報を比較したいという方も多いと思います。その比較がしやすくなるだろうというところも、この記事を書いた理由の一つです。
是非、SwiftData と比較する際は、この記事の情報などを参考にしてみてください。