WWDC Quick Look 💓 By SwiftGGTeam
What's new in SwiftData

What's new in SwiftData

元の動画を見る

ハイライト

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 内の @QueryFetchDescriptor を確認し、ソート・フィルタに頻繁に使うフィールドを特定。#Index でマーク。データ量小では差は小さいが、増加後は数倍の性能向上。

  • PreviewModifier でプレビューデータを統一:各ビューの #Preview で手動モデルコンテナ・サンプルデータ作成を避ける。PreviewModifier を定義し SwiftData データが必要な全プレビューが同一データソースを再利用。プレビュー一貫性向上、サンプルデータ変更も 1 箇所で済む。

関連セッション

コメント

GitHub Issues · utterances