SwiftData

SwiftDataは「SwiftUIに最適化されたCore Dataの代替」として2023年のWWDC23でAppleが発表した、SwiftとSwiftUIのためのデータ永続化フレームワークです。「宣言的でSwiftらしいデータモデリング」をコンセプトに、@Modelマクロによる直感的なモデル定義、@Queryによる自動的なSwiftUIビュー更新、CloudKitとの自動同期等を提供。Core Dataの堅牢なストレージエンジンを活用しながら、現代的なSwift開発体験を実現するAppleの次世代データ永続化ソリューションです。

Core DataSwiftiOSmacOSSwiftUIデータ永続化Apple

GitHub概要

swiftlang/swift

The Swift Programming Language

ホームページ:https://swift.org
スター68,845
ウォッチ2,437
フォーク10,499
作成日:2015年10月23日
言語:C++
ライセンス:Apache License 2.0

トピックス

なし

スター履歴

swiftlang/swift Star History
データ取得日時: 2025/7/19 08:07

ライブラリ

SwiftData

概要

SwiftDataは「SwiftUIに最適化されたCore Dataの代替」として2023年のWWDC23でAppleが発表した、SwiftとSwiftUIのためのデータ永続化フレームワークです。「宣言的でSwiftらしいデータモデリング」をコンセプトに、@Modelマクロによる直感的なモデル定義、@Queryによる自動的なSwiftUIビュー更新、CloudKitとの自動同期等を提供。Core Dataの堅牢なストレージエンジンを活用しながら、現代的なSwift開発体験を実現するAppleの次世代データ永続化ソリューションです。

詳細

SwiftData 2025年版はAppleエコシステムにおけるデータ永続化の新しい標準として位置づけられています。マクロベースの宣言的API(@Model、@Query、@Relationship)により、従来のCore Dataより大幅に簡潔なコード記述を実現。SwiftUIとの深い統合により、データ変更が自動的にUIに反映される反応的プログラミングを標準でサポート。iOS 17.0+、macOS 14.0+等の最新プラットフォーム対応で、CloudKit同期、マイグレーション、バックアップ・リストア等のエンタープライズ機能も完備。ただし、発表から1年以上経過した現在でも安定性の課題が指摘されており、プロダクション採用には慎重な検討が必要です。

主な特徴

  • 宣言的API: @Modelマクロによる直感的なデータモデル定義
  • SwiftUI統合: @Queryによる自動UI更新と反応的プログラミング
  • CloudKit同期: デバイス間データ同期の自動化
  • 型安全: Swift型システムとの完全統合
  • マイグレーション: スキーマ変更への自動対応
  • Core Data互換: Core Dataストレージエンジンの活用

メリット・デメリット

メリット

  • Core Dataより圧倒的に簡潔なコード記述で開発効率向上
  • SwiftUIとの深い統合により反応的UIの実装が容易
  • マクロベースのAPIでボイラープレートコード大幅削減
  • CloudKitとの自動同期でマルチデバイス対応が簡単
  • Swift型システムとの完全統合でタイプセーフティ確保
  • 最新のSwift言語機能(macros、property wrappers)活用

デメリット

  • iOS 17+、macOS 14+の最新プラットフォームのみ対応
  • 発表から1年以上経過もバグや不安定性の報告が多数
  • Core Dataに比べて機能が限定的で高度な要件に未対応
  • プロダクション環境での実績が少なく信頼性に不安
  • 複雑なデータモデルやカスタマイゼーションが困難
  • デバッグツールやドキュメントがCore Dataより限定的

参考ページ

書き方の例

インストールと基本設定

// iOS 17.0+, macOS 14.0+, watchOS 10.0+, tvOS 17.0+
import SwiftUI
import SwiftData

// アプリケーション設定
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [User.self, Post.self])
    }
}

// カスタム設定でのモデルコンテナ
@main
struct MyApp: App {
    var container: ModelContainer
    
    init() {
        let schema = Schema([User.self, Post.self])
        let config = ModelConfiguration(
            schema: schema,
            isStoredInMemoryOnly: false,
            allowsSave: true,
            cloudKitDatabase: .automatic
        )
        
        do {
            container = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("Failed to configure SwiftData container: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

基本的なモデル定義

import SwiftData
import Foundation

// 基本的なモデル定義
@Model
class User {
    @Attribute(.unique) var id: UUID
    var name: String
    var email: String
    var age: Int
    var isActive: Bool
    var createdAt: Date
    var updatedAt: Date
    
    // リレーションシップ
    @Relationship(deleteRule: .cascade, inverse: \Post.author)
    var posts: [Post] = []
    
    @Relationship(deleteRule: .nullify)
    var profile: UserProfile?
    
    init(name: String, email: String, age: Int) {
        self.id = UUID()
        self.name = name
        self.email = email
        self.age = age
        self.isActive = true
        self.createdAt = Date()
        self.updatedAt = Date()
    }
}

@Model
class Post {
    @Attribute(.unique) var id: UUID
    var title: String
    var content: String
    var isPublished: Bool
    var publishedAt: Date?
    var createdAt: Date
    var updatedAt: Date
    
    // 逆リレーションシップ
    var author: User?
    
    @Relationship(deleteRule: .cascade, inverse: \Comment.post)
    var comments: [Comment] = []
    
    init(title: String, content: String, author: User) {
        self.id = UUID()
        self.title = title
        self.content = content
        self.isPublished = false
        self.createdAt = Date()
        self.updatedAt = Date()
        self.author = author
    }
    
    func publish() {
        isPublished = true
        publishedAt = Date()
        updatedAt = Date()
    }
}

@Model
class Comment {
    @Attribute(.unique) var id: UUID
    var content: String
    var createdAt: Date
    
    var post: Post?
    var author: User?
    
    init(content: String, post: Post, author: User) {
        self.id = UUID()
        self.content = content
        self.createdAt = Date()
        self.post = post
        self.author = author
    }
}

@Model
class UserProfile {
    @Attribute(.unique) var id: UUID
    var bio: String
    var website: String?
    var location: String?
    var avatar: Data?
    
    var user: User?
    
    init(bio: String, website: String? = nil, location: String? = nil) {
        self.id = UUID()
        self.bio = bio
        self.website = website
        self.location = location
    }
}

SwiftUIでのデータ操作

import SwiftUI
import SwiftData

// メインビュー
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var users: [User]
    @Query(sort: \Post.createdAt, order: .reverse) private var recentPosts: [Post]
    
    var body: some View {
        NavigationView {
            List {
                Section("Users") {
                    ForEach(users) { user in
                        NavigationLink(destination: UserDetailView(user: user)) {
                            UserRowView(user: user)
                        }
                    }
                    .onDelete(perform: deleteUsers)
                }
                
                Section("Recent Posts") {
                    ForEach(recentPosts.prefix(5)) { post in
                        PostRowView(post: post)
                    }
                }
            }
            .navigationTitle("SwiftData Demo")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink("Add User", destination: AddUserView())
                }
            }
        }
    }
    
    private func deleteUsers(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(users[index])
            }
        }
    }
}

// ユーザー行表示
struct UserRowView: View {
    let user: User
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(user.name)
                .font(.headline)
            Text(user.email)
                .font(.caption)
                .foregroundColor(.secondary)
            HStack {
                Text("Age: \(user.age)")
                Spacer()
                Text("\(user.posts.count) posts")
                if user.isActive {
                    Image(systemName: "circle.fill")
                        .foregroundColor(.green)
                        .font(.caption)
                }
            }
            .font(.caption)
            .foregroundColor(.secondary)
        }
    }
}

// ユーザー追加ビュー
struct AddUserView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss
    
    @State private var name = ""
    @State private var email = ""
    @State private var age = 25
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $name)
                TextField("Email", text: $email)
                    .keyboardType(.emailAddress)
                Stepper("Age: \(age)", value: $age, in: 18...100)
            }
            .navigationTitle("Add User")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Save") {
                        saveUser()
                    }
                    .disabled(name.isEmpty || email.isEmpty)
                }
            }
        }
    }
    
    private func saveUser() {
        let newUser = User(name: name, email: email, age: age)
        modelContext.insert(newUser)
        
        do {
            try modelContext.save()
            dismiss()
        } catch {
            print("Failed to save user: \(error)")
        }
    }
}

条件付きクエリとフィルタリング

import SwiftUI
import SwiftData

// 条件付きクエリを使用するビュー
struct FilteredUsersView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var searchText = ""
    @State private var selectedAgeRange = 0
    @State private var showActiveOnly = false
    
    // 動的クエリの構築
    private var filteredUsersQuery: Query<User, [User]> {
        var predicate = #Predicate<User> { user in
            true // ベースとなる条件
        }
        
        // 検索テキストによるフィルタリング
        if !searchText.isEmpty {
            let searchPredicate = #Predicate<User> { user in
                user.name.localizedStandardContains(searchText) ||
                user.email.localizedStandardContains(searchText)
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && searchPredicate.evaluate(user)
            }
        }
        
        // アクティブユーザーのみのフィルタリング
        if showActiveOnly {
            let activePredicate = #Predicate<User> { user in
                user.isActive == true
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && activePredicate.evaluate(user)
            }
        }
        
        // 年齢範囲によるフィルタリング
        switch selectedAgeRange {
        case 1: // 18-30
            let agePredicate = #Predicate<User> { user in
                user.age >= 18 && user.age <= 30
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        case 2: // 31-50
            let agePredicate = #Predicate<User> { user in
                user.age >= 31 && user.age <= 50
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        case 3: // 51+
            let agePredicate = #Predicate<User> { user in
                user.age >= 51
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        default:
            break
        }
        
        return Query(filter: predicate, sort: \User.name)
    }
    
    var body: some View {
        VStack {
            // フィルタリングコントロール
            VStack(spacing: 16) {
                SearchBar(text: $searchText)
                
                Picker("Age Range", selection: $selectedAgeRange) {
                    Text("All Ages").tag(0)
                    Text("18-30").tag(1)
                    Text("31-50").tag(2)
                    Text("51+").tag(3)
                }
                .pickerStyle(SegmentedPickerStyle())
                
                Toggle("Active Users Only", isOn: $showActiveOnly)
            }
            .padding()
            
            // フィルタリングされたユーザーリスト
            FilteredUsersList(query: filteredUsersQuery)
        }
        .navigationTitle("Filtered Users")
    }
}

// クエリ結果を表示するビュー
struct FilteredUsersList: View {
    let query: Query<User, [User]>
    
    var body: some View {
        List(query.wrappedValue) { user in
            UserRowView(user: user)
        }
    }
}

// 検索バーコンポーネント
struct SearchBar: View {
    @Binding var text: String
    
    var body: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.secondary)
            
            TextField("Search users...", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            if !text.isEmpty {
                Button(action: {
                    text = ""
                }) {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                }
            }
        }
    }
}

データマイグレーションとバージョニング

import SwiftData

// バージョン1のスキーマ
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [UserV1.self, PostV1.self]
    }
    
    @Model
    class UserV1 {
        var name: String
        var email: String
        var age: Int
        
        init(name: String, email: String, age: Int) {
            self.name = name
            self.email = email
            self.age = age
        }
    }
    
    @Model
    class PostV1 {
        var title: String
        var content: String
        
        init(title: String, content: String) {
            self.title = title
            self.content = content
        }
    }
}

// バージョン2のスキーマ(新しいフィールドを追加)
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [UserV2.self, PostV2.self]
    }
    
    @Model
    class UserV2 {
        @Attribute(.unique) var id: UUID
        var name: String
        var email: String
        var age: Int
        var isActive: Bool
        var createdAt: Date
        
        init(name: String, email: String, age: Int) {
            self.id = UUID()
            self.name = name
            self.email = email
            self.age = age
            self.isActive = true
            self.createdAt = Date()
        }
    }
    
    @Model
    class PostV2 {
        @Attribute(.unique) var id: UUID
        var title: String
        var content: String
        var isPublished: Bool
        var createdAt: Date
        
        init(title: String, content: String) {
            self.id = UUID()
            self.title = title
            self.content = content
            self.isPublished = false
            self.createdAt = Date()
        }
    }
}

// マイグレーションプランの定義
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }
    
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            // マイグレーション前処理
            print("Starting migration from V1 to V2")
        },
        didMigrate: { context in
            // マイグレーション後処理
            print("Completed migration from V1 to V2")
            
            // デフォルト値の設定
            let users = try context.fetch(FetchDescriptor<SchemaV2.UserV2>())
            for user in users {
                if user.id == UUID(uuidString: "00000000-0000-0000-0000-000000000000") {
                    user.id = UUID()
                }
                user.isActive = true
                user.createdAt = Date()
            }
            
            let posts = try context.fetch(FetchDescriptor<SchemaV2.PostV2>())
            for post in posts {
                if post.id == UUID(uuidString: "00000000-0000-0000-0000-000000000000") {
                    post.id = UUID()
                }
                post.isPublished = false
                post.createdAt = Date()
            }
            
            try context.save()
        }
    )
}

// マイグレーション付きアプリケーション設定
@main
struct MigratableApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [SchemaV2.UserV2.self, SchemaV2.PostV2.self], migrationPlan: MigrationPlan.self)
    }
}

CloudKitとの統合

import SwiftUI
import SwiftData
import CloudKit

// CloudKit統合設定
@main
struct CloudKitApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [User.self, Post.self]) { result in
            switch result {
            case .success(let container):
                // CloudKit設定
                container.cloudKitDatabase = .automatic
                container.persistentStoreDescriptions.forEach { storeDescription in
                    storeDescription.setOption(true as NSNumber,
                                             forKey: NSPersistentHistoryTrackingKey)
                    storeDescription.setOption(true as NSNumber,
                                             forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
                }
            case .failure(let error):
                print("Failed to create container: \(error)")
            }
        }
    }
}

// CloudKit同期状態の監視
struct CloudKitSyncView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var syncStatus: CloudKitSyncStatus = .unknown
    @State private var lastSyncDate: Date?
    
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            HStack {
                Text("Sync Status:")
                Spacer()
                StatusIndicator(status: syncStatus)
            }
            
            if let lastSync = lastSyncDate {
                HStack {
                    Text("Last Sync:")
                    Spacer()
                    Text(lastSync, style: .relative)
                        .foregroundColor(.secondary)
                }
            }
            
            Button("Force Sync") {
                forceSyncWithCloudKit()
            }
            .disabled(syncStatus == .syncing)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
        .onAppear {
            monitorSyncStatus()
        }
    }
    
    private func forceSyncWithCloudKit() {
        syncStatus = .syncing
        
        // SwiftDataではCloudKit同期は自動的に行われるため、
        // 手動での強制同期は通常不要
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            syncStatus = .synced
            lastSyncDate = Date()
        }
    }
    
    private func monitorSyncStatus() {
        // CloudKit同期状態の監視実装
        // 実際の実装では、NSPersistentCloudKitContainerのイベント監視を行う
    }
}

enum CloudKitSyncStatus {
    case unknown, syncing, synced, error
}

struct StatusIndicator: View {
    let status: CloudKitSyncStatus
    
    var body: some View {
        HStack {
            switch status {
            case .unknown:
                Image(systemName: "questionmark.circle")
                    .foregroundColor(.gray)
                Text("Unknown")
            case .syncing:
                ProgressView()
                    .scaleEffect(0.8)
                Text("Syncing...")
            case .synced:
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                Text("Synced")
            case .error:
                Image(systemName: "exclamationmark.triangle.fill")
                    .foregroundColor(.red)
                Text("Error")
            }
        }
        .font(.caption)
    }
}