ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftDataの新機能
SwiftDataを利用すると、表現力に優れた宣言型のAPIによって、アプリのデータを簡単に永続化できます。このセッションでは、複合的な一意性制約、#indexによるクエリの高速化、Xcodeプレビューでのクエリ、豊富な述語表現など、SwiftDataの改善点について解説します。これらのさまざまな機能を使用してより充実したモデルを表現し、アプリのパフォーマンスを向上させたいデベロッパの方は、ぜひご参加ください。SwiftDataでカスタムデータストアを構築する方法や履歴に関するAPIを使用する方法については、「Create a custom data store with SwiftData(SwiftDataを使用したカスタムデータストアの作成)」と「Track model changes with SwiftData history(SwiftDataの履歴機能によるモデル変更のトラッキング)」をご視聴ください。
関連する章
- 0:00 - Introduction
- 0:57 - Adopt SwiftData
- 2:11 - Customize the schema
- 2:43 - #Unique macro
- 3:37 - History API
- 4:29 - Tailor a model container
- 5:39 - Custom data stores
- 6:41 - Xcode previews
- 9:20 - Customize queries
- 10:18 - #Expression macro
- 11:56 - #Index macro
リソース
関連ビデオ
WWDC24
WWDC23
-
ダウンロード
こんにちは Rishi Vermaです SwiftDataチームで エンジニアをしています 本日はSwiftDataの新機能 について説明します
SwiftDataは iOS 17で導入されました このフレームワークにより Appleのすべてのプラットフォームで Swiftにおけるアプリデータのモデル化と 永続化が可能になります マクロなど Swift言語の 最新機能を活用して 高速で効率的かつ安全な コードを記述できます このビデオでは SwiftDataフレームワークの 概要を簡単に復習してから 新機能を掘り下げていきます まず 新しいスキーママクロを使って モデルの重複を避ける方法を紹介し 次に モデルコンテナを設定して 構成するための新しい方法を説明します
最後に 複雑なフィルタを使って クエリを最適化する方法と 新しいマクロでパフォーマンスを 向上させる方法を説明します
まず SwiftDataについて 簡単に説明します SwiftDataは アプリのモデルレイヤを簡単に構築し アプリの起動間でそれを 永続化できるフレームワークです
このフレームワークは永続化だけでなく スキーマのモデル化と移行 グラフ管理 CloudKitとの同期など 多数の機能を提供します SwiftDataをアプリに導入するのが いかに簡単かを説明するために 私とチームが開発しているアプリ Tripsを紹介します TripsはSwiftUIで書かれたアプリで 旅行について検討したことなど 様々な記録をすべて残せます
このアプリのモデルで SwiftDataを使用するには フレームワークをインポートし 各モデルに@Modelマクロを 追加するだけです これがSwiftDataの強力な機能です
アプリの定義では WindowGroupの modelContainerモディファイアが Tripモデルのビュー階層全体に 通知します
これが設定されると ビューは静的データを削除し 代わりに@Queryを使って ビューを構成します これにより モデルコンテナから Tripモデルを取得しTripsの配列を返します これで完了です! アプリは私が作成した 旅行をすべて永続化し SwiftUIビューに完璧にフィットします 最初のステップは@Modelマクロを追加 することでしたが これはスキーマのカスタマイズ方法の 始まりに過ぎません
@Modelマクロは強力で 永続化の経験に役立ちます 永続化可能なすべてのクラスに このマクロを付けるだけで Tripクラスと関連モデルの 保存プロパティが永続化されます
さらに @Attributesや @Relationshipsのマクロを使って スキーマをカスタマイズしたり 保存プロパティを @Transientと指定して データの永続化を回避することもできます
今年は新しいスキーママクロが導入され 永続モデルに対する 複合制約を構築できるようになりました
新しい#Uniqueマクロを使用して モデルのプロパティの どの組み合わせがモデルデータ内で 一意でなければならないのかを SwiftDataに指示できます 2つのモデルインスタンスが 同じ一意の値を共有する場合 SwiftDataは既存モデルと競合した際に アップサートを実行します
例えば Tripsアプリでは #Uniqueマクロを使って 旅行の名前 開始日 終了日が 一意であることを保証しています これでこのアプリは 同じ名前の 複数の旅行を持つことができますが 開始日と終了日が異なる場合に限ります これにより データの重複を避けることが とても簡単になります なぜなら SwiftDataは より多くの情報に基づいて 実際にどのモデルが重複しているかを判断し 代わりにデータの更新を実行するからです
#Uniqueプロパティで@Modelが 重複していないことを確認できるため これらのプロパティは このモデルの識別子にもなります @Attributeマクロを使って preserveValueOnDeletionを これらのプロパティに 指定することもできます これにより SwiftDataで 履歴APIを使う際に これらの識別値を利用できます
SwiftDataの履歴機能を使うと 挿入 更新 削除されたモデルを 時間の経過とともに アプリで把握できます モデルが削除されると 保存するように指定された値は トゥームストーン値として 履歴情報に保持され これらの変更を処理するために必要な情報が アプリに提供されます また これをサポートするよう構築された カスタムデータストアとシームレスに連携します 詳細はこちらのビデオ 「Track model changes with SwiftData history」 をご覧ください モデルコンテナをカスタマイズすると データの場所とアプリ全体での データの使い方をアプリで微調整できます
modelContainerモディファイアは SwiftDataを使う最も簡単な方法です 永続化するモデルの型を指定するだけで SwiftDataがコンテナを用意します modelContainerモディファイアで コンテナの一部のプロパティを カスタマイズすることもできます
例えば ディスクではなく メモリにデータを保存したり 自動保存を有効または無効にしたり 元に戻す/やり直しの オンとオフを切り替えたりできます
modelContainerを さらにカスタマイズして ディスク上の保存先の変更などを行うには 独自のmodelContainer インスタンスを別に構築します Tripsアプリでこれを試してみましょう modelContainerモディファイアで コンテナを構築する代わりに containerプロパティを使って 独自のコンテナを作成します
このプロパティのクロージャで モデルの構成を作成してスキーマを渡します ここでディスク上のデータの URLもカスタマイズします 次に ModelContainerイニシャライザに この構成を渡して 結果を返します
iOS 18のSwiftDataでは 完全カスタムのデータストアを使って modelContainerを さらにカスタマイズできます デフォルトのデータストアは SwiftDataの全機能をサポートする 堅牢な永続性バックエンドを提供しますが ここでは 独自のデータストアを作成します これにより 独自の実装で コンテナ全体のデータを永続化します
例えば Tripsアプリでは JSONファイルで構成された 独自の カスタムドキュメント形式を実装しました これをアプリで使用するためには モデルの構成を カスタムデータストアで指定した 構成に入れ替えるだけです この例では JSONStoreConfigurationです
カスタムデータストアでは データの永続化形式を問わず @Modelや@Queryマクロなどの 使い慣れたSwiftData APIを 使うことができます データストアに機能を段階的に 取り入れることもできるため すぐに使い始めることができます 詳細についてはこちらのビデオ 「Create a custom data store with SwiftData」をご覧ください
Xcodeのプレビュー用に カスタムコンテナを作成することもできます プレビューは SwiftUIでのアプリ開発において 最適なツールであり SwiftDataと非常に相性がいいです
Tripsアプリのすべてのビューで 優れたプレビューを作成したいと思います まず プレビュー特性を使います
そのために SampleDataという 新しい構造体を作成し PreviewModifierに準拠させます この構造体には 実装する必要のある 2つの関数があります 1つはプレビュー用の 共有コンテキストを設定するためのもので もう1つは共有コンテキストを ビューに適用するためのものです Tripsのプレビューでは ModelContainerをsampleDataの 共有コンテキストとして用意します プレビューではディスクに 何も保存する必要がないので データをメモリにのみ保存する ModelConfigurationを作成し ModelContainerを設定します
次に 先ほど作成したメソッドを呼び出します これは様々なサンプル旅行を作成し モデルコンテナに保存します 旅行は名前と日付が一意であるため このコードではデータの重複排除は不要です SwiftDataが行ってくれます
最後にコンテナを返します 次に sampleDataを使用するビューに このmodelContainerを 追加するメソッドを 実装する必要があります そのためには modelContainerモディファイアを使用して このコンテナを適用するだけです
最後に PreviewTraitに 拡張機能を追加します これで このsampleDataに 簡単にアクセスできます これにより 新しい静的プロパティ sampleData()が作成され SampleData()構造体が モディファイアとして適用されます
これで 任意のSwiftUIビューの プレビュー宣言時に traitsパラメータで .sampleDataを 使用できます これにより メモリ内のモデルコンテナが作成され sampleDataが読み込まれ SwiftUIビューで 使えるようにプレビューが変更されます
優れたサンプルデータを用意すれば SwiftDataクエリを使って アプリの どのビューでも簡単に作業できます ただし 一部のアプリのビューは モデルが渡されることに依存しているため クエリが含まれていない可能性があります この場合も @Previewableマクロで 素晴らしいプレビューを作成できます
例えばTripsでは BucketListItemViewは1つの旅行を パラメータとして受け取ります sampleDataを使用すると bucketListItemViewは sampleDataを含む モデルコンテナを持ちますが まだそのデータのクエリを実行していません
今は @Previewableマクロを使用し プレビュー宣言内で 直接クエリを作成できます これにより 提供される旅行の配列を BucketListItemViewに渡すことができ sampleDataを使ったプレビューを 作成できます
最後に SwiftData向けに 最適化された 強力なクエリの作成方法を紹介します クエリは ソートやフィルタリングを 簡単に行えるモデルの配列で SwiftUIビューを駆動し ModelContainerに加えられた 変更に自動的に反応します #Predicateは フィルタリングを容易にし データクエリの実行時に評価できます 大規模なメモリ内データセットを 使う場合とは対照的です Tripsでフィルタを使う方法を いくつか説明します Tripsアプリに検索バーを追加する場合 searchTextでクエリの フィルタリングや取得用の 述語を構築できます
述語の構築は簡単です 指定されたsearchTextを取得し 旅行名にそのテキストが 含まれているか確認します ただし テキストは旅行名以外に 適用される場合があります
そこで複合述語を構築して 旅行のdestinationプロパティも確認します これで複合述語は完成しましたが 述語では他にも様々なことを行えます
iOS 18で導入された Foundationの新しい #Expressionマクロを使うと 複雑な述語を簡単に構築できます
Expressionは trueやfalseを生成しない参照値を許可し 代わりに任意の型を許可します
Expressionでは モデルのプロパティを使って 複雑な評価を表すことができます 述語内に組み込むと クエリの結果をさらにカスタマイズできます
Tripsアプリでクエリを作成して まだ訪れていない場所がある 進行中の旅行を取得しましょう これらは isInPlanプロパティが まだfalseである旅行の BucketListItemで モデル化されます まず 述語を構築します
この述語で 旅行が 継続中であることを指定します つまり 現在の日付は 開始日と終了日の間です
ただし 旅行のBucketListItemの 少なくとも1つでisInPlanプロパティが falseであることを指定する 必要があります 述語だけではこれを表現できません なぜなら 計画外のBucketListItemの数を カウントするプロパティがないからです そこで このロジックを述語に 組み込む式を作ります
この式は まだ計画していない BucketListItemの数をカウントします これはBucketListItemの配列を受け取り フィルタ条件を満たす項目の数を返します
これで 指定された旅行の BucketListItemを使って 述語の一部としてこの式を評価できます さらに この述語では 式の結果が 0より大きいかどうかを チェックできます Expressionを使うと述語マクロが クエリを記述するための より強力で優れたツールとなり アプリに必要なデータを 効果的に取得できます 実は クエリのパフォーマンスを 向上させるもう1つの方法があります 新しいスキーママクロ #Indexを使う方法です #Indexは モデルに1つのインデックス または複合インデックスを 作成する機能を追加します インデックスは 本の目次のような 追加のメタデータを表します このメタデータはSwiftDataが 生成してコンテナに保存します このメタデータを使うと 指定されたキーパスに対するクエリが 高速で効率的になります
このメリットを得るには どのプロパティにSwiftDataが インデックスを作成するかを宣言します クエリの並べ替えやフィルタリングで 最も頻繁に使用する プロパティについて考えます
Tripsアプリの場合 旅行のクエリで名前 開始日 終了日の プロパティを使ったフィルタリングや 並べ替えが頻繁に行われます このクエリを高速にするには #Indexマクロを追加し 名前 開始日 終了日の キーパスと これら3つの 複合インデックスを指定します 旅行の詳細な検討内容のような 大規模データセットの場合 この方法でフィルタリングと 並べ替えが大幅に高速になります 述語マクロを使うことで SwiftUIでのクエリの使い勝手が向上し Expressionを使うことで より強力になります #Indexマクロを使うと アプリ内でクエリのパフォーマンスを 一層向上させることができます
SwiftDataのパワーを使って アプリのモデルレイヤを構築しましょう #Unique制約をスキーマに追加して モデルの重複回避を容易にしましょう 新しい#Indexマクロを追加して クエリを高速化し
新しい履歴APIで アプリのモデルに対する変更を追跡できます カスタムデータストアを使うと 独自のドキュメント形式または 永続性バックエンドにより SwiftDataのパワーを活用できます
ご視聴ありがとうございました 皆さんが作り上げる 素晴らしいアプリを楽しみにしています
-
-
1:32 - SampleTrips models decorated with @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem {...} @Model class LivingAccommodation {...}
-
1:43 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView } .modelContainer(for: Trip.self) } }
-
1:53 - SampleTrips using @Query
// Trip App using @Query import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { NavigationSplitView { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) } } } } }
-
2:16 - SampleTrips models decorated with @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem {...} @Model class LivingAccommodation {...}
-
3:08 - Add unique constraints to avoid duplication
// Add unique constraints to avoid duplication import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? }
-
3:36 - Add .preserveValueOnDeletion to capture unique columns
// Add .preserveValueOnDeletion to capture unique columns import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) @Attribute(.preserveValueOnDeletion) var name: String var destination: String @Attribute(.preserveValueOnDeletion) var startDate: Date @Attribute(.preserveValueOnDeletion) var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? }
-
4:35 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
4:52 - Customize a model container in the app
// Customize a model container in the app import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, inMemory: true, isAutosaveEnabled: true, isUndoEnabled: true) } }
-
5:13 - Add a model container to the app
// Add a model container to the app import SwiftUI import SwiftData @main struct TripsApp: App { var container: ModelContainer = { do { let configuration = ModelConfiguration(schema: Schema([Trip.self]), url: fileURL) return try ModelContainer(for: Trip.self, configurations: configuration) } catch { ... } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
5:59 - Use your own custom data store
// Use your own custom data store import SwiftUI import SwiftData @main struct TripsApp: App { var container: ModelContainer = { do { let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: jsonFileURL) return try ModelContainer(for: Trip.self, configurations: configuration) } catch { ... } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
6:58 - Make preview data using traits
// Make preview data using traits struct SampleData: PreviewModifier { static func makeSharedContext() throws -> ModelContainer { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: Trip.self, configurations: config) Trip.makeSampleTrips(in: container) return container } func body(content: Content, context: ModelContainer) -> some View { content.modelContainer(context) } } extension PreviewTrait where T == Preview.ViewTraits { @MainActor static var sampleData: Self = .modifier(SampleData()) }
-
8:15 - Use sample data in a preview
// Use sample data in a preview import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { ... } } #Preview(traits: .sampleData) { ContentView() }
-
8:50 - Create a preview query using @Previewable
// Create a preview query using @Previewable import SwiftUI import SwiftData #Preview(traits: .sampleData) { @Previewable @Query var trips: [Trip] BucketListItemView(trip: trips.first) }
-
9:55 - Create a predicate to find a Trip based on search text
// Create a Predicate to find a Trip based on Search Text let predicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) }
-
10:06 - Create a Compound Predicate to find a Trip based on Search Text
// Create a Compound Predicate to find a Trip based on Search Text let predicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) || $0.destination.localizedStandardContains(searchText) }
-
10:46 - Build a predicate to find Trips with BucketListItems that are not in the plan
// Build a predicate to find Trips with BucketListItems that are not in the plan let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in items.filter { !$0.isInPlan }.count } let today = Date.now let tripsWithUnplannedItems = #Predicate<Trip>{ trip // The current date falls within the trip (trip.startDate ..< trip.endDate).contains(today) && // The trip has at least one BucketListItem // where 'isInPlan' is false unplannedItemsExpression.evaluate(trip.bucketList) > 0 }
-
12:41 - Add Index for commonly used KeyPaths or combination of KeyPaths
// Add Index for commonly used KeyPaths or combination of KeyPaths import SwiftData @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate]) var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem var livingAccommodation: LivingAccommodation }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。