ハイライト
SwiftData は複合一意制約用
#Uniqueマクロ、クエリ高速化用#Indexマクロ、カスタムデータストア実装のサポートを追加しました。
主要内容
SwiftData は昨年 iOS 17 とともに登場し、マクロなど Swift のモダン言語機能でデータ永続化を簡素化することを目指しました。開発者は @Model でクラスを装飾するだけで永続化能力を自動取得し、@Query で SwiftUI ビューからデータを取得します。
今年は実用的な問題 — データ重複 — を解決します。ユーザーがデータ同期、一括インポート、ネットワークリトライを行うと、重複モデルインスタンスが発生しやすくなります。以前は手動で重複排除するか、単一の一意識別子に依存する必要がありました。しかし多くのビジネスシナリオでは、一意性は複数フィールドの組み合わせ — 旅行 app では同名行程でも日付が異なれば許可、「名称+開始日+終了日」がすべて一致した場合のみ真の重複。
SwiftData は #Unique マクロを導入し、複数プロパティの複合一意制約を宣言できます。挿入モデルがこれらの一意フィールドで既存モデルと競合すると、SwiftData は自動 upsert(挿入ではなく更新)を実行し、根本から重複を防止します。
もう一つの性能問題はクエリ効率。データ量増加に伴い、特定フィールドでの頻繁なソート・フィルタが遅くなります。#Index マクロでデータベースインデックスを宣言 — 本の目次のように — これらの操作を大幅に高速化。さらに SwiftData は完全カスタムデータストアをサポートし、SQLite バックエンドに限定されず、JSON や他フォーマット、完全カスタム永続化方式も使用可能です。
詳細
複合一意制約
#Unique マクロは KeyPath のセットを受け取り、これらのプロパティ組み合わせが一意であることを宣言(03:08):
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?
}
#Unique<Trip>で Trip モデルの一意制約を宣言\.name, \.startDate, \.endDateの 3 プロパティ組み合わせが一意である必要- 3 フィールドすべて一致する挿入モデルは新規挿入ではなく既存モデルを更新
これらの一意フィールドはモデルの identity も表します。モデル削除後も SwiftData History API が identity を識別できるよう @Attribute(.preserveValueOnDeletion) を追加(03:36):
@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?
}
@Attribute(.preserveValueOnDeletion)でモデル削除後もこれらのフィールド値を履歴に保持- History API はモデルの挿入・更新・削除を追跡、保持値が tombstone 情報として変更処理を支援
カスタムデータストア
iOS 18 で完全カスタムデータストアが可能(05:59):
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)
}
}
JSONStoreConfigurationがデフォルトModelConfigurationを置き換え、カスタムデータストアを使用- カスタムストアは機能を段階的に実装して素早く開始可能
@Model、@Queryなど SwiftData API は引き続き使用可能
プレビューデータサポート
SwiftUI プレビューで PreviewModifier によりサンプルデータを作成可能(06:58):
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())
}
PreviewModifierでプレビューの共有コンテキストを定義isStoredInMemoryOnly: trueでプレビューがディスクに書き込まないことを保証Trip.makeSampleTripsでサンプルデータを投入
使用例(08:15):
#Preview(traits: .sampleData) {
ContentView()
}
モデルパラメータに依存するビューでは @Previewable マクロでプレビュー内にクエリを作成(08:50):
#Preview(traits: .sampleData) {
@Previewable @Query var trips: [Trip]
BucketListItemView(trip: trips.first)
}
@Previewableでプレビュー宣言内に@Queryを作成可能- 返却配列をビューに直接渡せる
複雑なクエリと式
#Predicate で複合フィルタ条件を構築(09:55):
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true :
$0.name.localizedStandardContains(searchText) ||
$0.destination.localizedStandardContains(searchText)
}
iOS 18 で #Expression マクロ追加、非ブール値の複雑計算をサポート(10:46):
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
items.filter {
!$0.isInPlan
}.count
}
let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
(trip.startDate ..< trip.endDate).contains(today) &&
unplannedItemsExpression.evaluate(trip.bucketList) > 0
}
#Expression<[BucketListItem], Int>で配列入力・整数出力の式を定義- 式は配列内の条件一致要素数を計算
evaluate(trip.bucketList)で述語内から式を呼び出し- 「配列内に条件を満たす要素が少なくとも 1 つ」など複雑ロジックを述語で表現可能
インデックスでクエリを高速化
#Index マクロで常用フィールドにインデックス作成(12:41):
@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?
}
#Index<Trip>で指定 KeyPath にインデックス作成[\.name],[\.startDate],[\.endDate]で単一フィールドインデックス[\.name, \.startDate, \.endDate]で 3 フィールド複合インデックス- これらのフィールドでのソート・フィルタ操作を高速化
- 大規模データセットで効果顕著
重要ポイント
-
ビジネス identity に一意制約を追加:自然な主キー(ユーザー ID、注文番号)があれば
#Uniqueで宣言。複合キー(ユーザー+時間)も同様。データ同期・一括インポートで重複レコードが発生せず、重複排除コードも不要。 -
高頻度クエリフィールドにインデックス追加:app 内の
@QueryとFetchDescriptorを確認し、ソート・フィルタに頻繁に使うフィールドを特定。#Indexでマーク。データ量小では差は小さいが、増加後は数倍の性能向上。 -
PreviewModifier でプレビューデータを統一:各ビューの
#Previewで手動モデルコンテナ・サンプルデータ作成を避ける。PreviewModifierを定義し SwiftData データが必要な全プレビューが同一データソースを再利用。プレビュー一貫性向上、サンプルデータ変更も 1 箇所で済む。
関連セッション
- Track model changes with SwiftData history — SwiftData History API でモデルの挿入・更新・削除を追跡
- Create a custom data store with SwiftData — SwiftData 用カスタムデータストア構築を深掘り
- Meet SwiftData — SwiftData 基礎:モデリングと app データの永続化
- Build complex queries with SwiftData — SwiftData クエリ、述語、リレーションシップを深掘り
コメント
GitHub Issues · utterances