iOS17から使用できるSwiftDataについて勉強してみました。
いつもは外部ライブラリーのRealmをよく使っているのですが、
外部ライブラリーはバージョンアップがなされなくなると使えなくなることがあるので(Realmは今のところそんなことにはならなそうですが..)Appleが提供しているデータ永続化のフレームワークを勉強しておこうと思います。
・SwiftDataって何?
iOS 17では、AppleはCoreDataフレームワークに代わるSwiftDataと呼ばれる新しいフレームワークを導入しました。
Developerの説明が英語なので解釈が合っているか不安ですが、まず「SwiftDataはデータベース自体とは違うよ」との事。
これはコアデータ上に構築されたフレームワークであり、開発者が永続的に保存されたデータを効果的に管理および操作できるよう開発されました。iOSで使用されるデフォルトの永続ストアは通常 SQLite データベースですが、永続ストアにはさまざまな形式があることに注意する必要があります。
たとえば、コアデータは、XMLファイルなどのローカルファイルに保存されているデータを管理するためにも使用できます。この柔軟性により、開発者は特定の要件に最適な永続ストアを選択できます。
Core Data と SwiftData フレームワークのどちらを選択するかにかかわらず、どちらのツールも、開発者向けの基礎となる永続ストアの複雑さを簡素化することを目的としています。
SQLiteデータベースを例にとった場合、SwiftDataを使用すると、データベース接続の確立や、データレコードを取得するためのSQLクエリを掘り下げを心配する必要はありません。代わりにユーザーフレンドリーなAPIと@ModelなどのSwiftマクロを活用して、アプリケーション内のデータを効率的に管理することできます。この抽象化により、より合理化された直感的なデータ管理エクスペリエンスが可能になります。
とりあえず書きながら勉強していこうと思います。
詳しくはAppleDeveloperなどをご参照下さい↓
https://developer.apple.com/xcode/swiftdata/
・参考
主にSwiftUIでの利用についての記事が多い中、UIkitでの使用方法をまとめている方がいらっしゃったので今回は参考にさせていただきました↓
https://qiita.com/gonnbe1106/items/dee82f4e5989005625f2
・使い方
まず保存するDataのClassを作ります。
「@ModelなどのSwiftマクロを活用」とありますので
保存したいデータのClassの頭に@Modelを付けます。
あえて今回は2つのClassを作ります。
@Model
class User {
var name: String
var uuid: UUID
init(name: String, uuid: UUID) {
self.name = name
self.uuid = uuid
}
}
@Model
class Number {
var seatNumber: Int
var uuid: UUID
init(seatNumber: Int, uuid: UUID) {
self.seatNumber = seatNumber
self.uuid = uuid
}
}
次にデータの永続的な操作を実行するためにModelContainerを作成します。
ModelContainerはモデルタイプの永続的なバックエンドとして機能します。
ModelContainerを作成するにはModelContainerをインスタンス化します。
※この際に注意しなければいけないのが、保存したいデータのClassが複数の場合にはModelContainerを配列にし全ての保存型のClassを全て入れる必要があります。
class SetlContainer {
// モデルコンテナの変数
var container: ModelContainer?
}
class CreateContainer {
// モデルコンテナの作成
var container: ModelContainer = {
// スキーマの定義
let schema = Schema([
User.self,
Number.self
])
// モデルコンテナの構成を設定
let modelConfiguretion = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
// モデルコンテナを設定して返す
return try ModelContainer(for: schema, configurations: [modelConfiguretion])
} catch {
// エラーが発生した場合は致命的なエラーを発生させる
fatalError("Could not create ModelContainer (ModelContainer を作成できませんでした): \(error)")
}
}()
// モデルコンテナを初期化する
func initializeModelContainer() -> ModelContainer {
// スキーマの定義
let schema = Schema([
User.self,
Number.self
])
// モデルコンテナの構成を設定
let modelConfiguretion = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
// モデルコンテナを設定して返す
return try ModelContainer(for: schema, configurations: [modelConfiguretion])
} catch {
// エラーが発生した場合は致命的なエラーを発生させる
fatalError("Could not create ModelContainer (ModelContainer を作成できませんでした): \(error)")
}
}
}
AppDelegateでアプリ起動時にコンテナの初期化をする。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// アプリの起動時にSwiftDataのコンテナを初期化
CreateContainer().container = CreateContainer().initializeModelContainer()
return true
}
データベースからデータを取得する。
データベースからデータを取得するには、FetchDescriptorのインスタンスを作成する必要があります。これにより、取得したいデータタイプを指定し、必要に応じて特定の検索条件を定義できます。FetchDescriptorを利用することで、データベースから目的のデータを効果的に取得できます。
データを全て取得してテーブルビューに表示する方法は多くの方が載せていたので、
今回は欲しいデータだけを取得し表示する方法に挑戦してみます。
// SwiftDataの取得
func getUserSd(for setName: String) {
do {
// #Predicateマクロを使用して、述語を構築
// UserNameに該当するDataを取得する
let userName = #Predicate<User> {$0.name == setName}
/*フェッチの基準を定義したらFetchDescriptorを使用して、
モデルコンテキストにデータを取得するように指示
*/
let descriptor = FetchDescriptor<User>(predicate: userName)
// コンテナのコンテキストの呼び出し
guard let userData = try container?.mainContext.fetch(descriptor) else {
// エラーが発生した場合の処理
print("SwiftData[User] Error: データの取得に失敗しました.")
return
}
// 名前に紐付いた取得できた場合の処理
// 名前を取得
guard let name = userData.first?.name else {
// エラーが発生した場合の処理
print("Get Name Error: ユーザー名が見つかりません.")
return
}
// ユニークIDを取得する
guard let id = userData.first?.uuid else {
// エラーが発生した場合の処理
print("Get UUID Error: ユニークIDが見つかりません.")
return
}
// 取得できた場合の処理
getNumberSd(for: name, uuid: id)
} catch {
// エラーが発生した場合の処理
print("SwiftData[User] Error: 予想しないErrorが発生しました.\(error)")
}
}
func getNumberSd(for name: String, uuid: UUID) {
do {
/* #Predicateマクロを使用して述語を構築
UUIDに該当するDataを取得する
*/
let userId = #Predicate<Number> {$0.uuid == uuid}
/*フェッチの基準を定義したらFetchDescriptorを使用して、
モデルコンテキストにデータを取得するように指示
*/
let descriptor = FetchDescriptor<Number>(predicate: userId)
guard let id = try container?.mainContext.fetch(descriptor) else {
// エラーが発生した場合の処理
print("SwiftData[Number] Error: データの取得に失敗しました.")
return
}
// idに紐付いた取得できた場合の処理
// idに紐付いたseatNumberを取得する
guard let seatNumber = id.first?.seatNumber else {
// エラーが発生した場合の処理
print("Get seatNumber Error: シートナンバーが見つかりません.")
return
}
// 取得できた場合の処置
} catch {
print("SwiftData[Number] Erro: 予想しないErrorが発生しました.\(error)")
}
}
SwftDataを使用したいView毎にviewDidLoad()内でコンテナのインスタンス化をする。
class ViewController: UIViewController {
//コンテナを保持する変数
var container = SetContainer().container
override func viewDidLoad() {
super.viewDidLoad()
// モデルコンテナのインスタンス化
container = CreateContainer().container
}
データを保存・追加する場合は .insert( ) を使う。
container?.mainContext.insert(User(name: name, uuid: uuid))
データを削除したい場合は .delete( ) を使う。
// 指定のデータを削除
container?.mainContext.delete(User(name: name, uuid: uuid))
// アイテムを完全に削除
do {
try container?.mainContext.delete(model: User.self)
} catch {
print("delete Error: 予想しないErrorが発生しました.\(error)")
}
これで実際にサンプルを作って動かしてみたいと思います。
・サンプル動画
・おわりに
無事にデータの保存と取得をすることができました!
まだまだ勉強の必要があるので、もっと使いこなせるように頑張ってみようと思います。