--- id: ios-swiftdata-patterns title: SwiftData — Modern Persistence (iOS 17+) category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, swiftdata, persistence, vibe-coding] tech_stack: { language: "Swift / SwiftData", applicable_to: ["iOS"] } applied_in: [] aliases: [SwiftData, @Model, ModelContainer, ModelContext, query, predicate] --- # SwiftData (iOS 17+) > Core Data 의 현대적 wrapper. **`@Model` macro + Swift native syntax**. SwiftUI 통합. iCloud 동기화 자동. 새 프로젝트 = SwiftData. ## 📖 핵심 개념 - @Model: persisted class. - ModelContainer: store. - ModelContext: 변경 단위 (transaction). - @Query: SwiftUI 자동 fetch. ## 💻 코드 패턴 ### Model ```swift import SwiftData @Model final class Note { @Attribute(.unique) var id: UUID var title: String var body: String var createdAt: Date @Relationship(deleteRule: .cascade) var tags: [Tag] = [] init(title: String, body: String) { self.id = UUID() self.title = title self.body = body self.createdAt = Date() } } @Model final class Tag { @Attribute(.unique) var name: String var notes: [Note] = [] init(name: String) { self.name = name } } ``` ### App setup ```swift @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Note.self, Tag.self]) } } ``` ### SwiftUI — @Query ```swift struct NotesList: View { @Query(sort: \Note.createdAt, order: .reverse) private var notes: [Note] @Environment(\.modelContext) private var ctx var body: some View { List { ForEach(notes) { note in NavigationLink(value: note) { Text(note.title) } } .onDelete(perform: delete) } } private func delete(at offsets: IndexSet) { for i in offsets { ctx.delete(notes[i]) } } } ``` ### Filter (Predicate) ```swift @Query(filter: #Predicate { $0.title.contains("Foo") }, sort: \.createdAt, order: .reverse) var notes: [Note] ``` ```swift // Dynamic @Query private var notes: [Note] init(searchText: String) { let predicate = #Predicate { $0.title.localizedStandardContains(searchText) } _notes = Query(filter: predicate, sort: \.createdAt, order: .reverse) } ``` ### CRUD ```swift // Create let note = Note(title: "Hello", body: "World") ctx.insert(note) try ctx.save() // 또는 자동 (SwiftUI) // Update — 그냥 변경 note.title = "Updated" try ctx.save() // Delete ctx.delete(note) try ctx.save() // Fetch (manual) let descriptor = FetchDescriptor( predicate: #Predicate { $0.title.contains("Foo") }, sortBy: [SortDescriptor(\.createdAt, order: .reverse)] ) let notes = try ctx.fetch(descriptor) ``` ### Relationship ```swift let note = Note(title: "...", body: "...") let tag = Tag(name: "important") note.tags.append(tag) ctx.insert(note) try ctx.save() // tag 도 자동 insert ``` ### Cascade delete ```swift @Relationship(deleteRule: .cascade) var tags: [Tag] // note 삭제 = tags 도 @Relationship(deleteRule: .nullify) var author: User? // note 삭제 = user.notes 에서 nullify ``` ### iCloud sync (CloudKit) ```swift .modelContainer(for: Note.self, isAutosaveEnabled: true) // Cloud 자동 sync — Bundle ID + iCloud capability + entitlement ``` ``` Capabilities → iCloud → CloudKit + Background Modes → Remote notifications ``` → 사용자가 iCloud 계정만 있으면 자동. ### Migration ```swift enum NotesV1: VersionedSchema { static var versionIdentifier = Schema.Version(1, 0, 0) static var models: [any PersistentModel.Type] = [Note.self] } enum NotesV2: VersionedSchema { static var versionIdentifier = Schema.Version(2, 0, 0) static var models: [any PersistentModel.Type] = [Note.self] } enum MigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] = [NotesV1.self, NotesV2.self] static var stages: [MigrationStage] = [ .lightweight(fromVersion: NotesV1.self, toVersion: NotesV2.self), ] } let container = try ModelContainer( for: Note.self, migrationPlan: MigrationPlan.self ) ``` ### Background context ```swift let context = ModelContext(container) // 또는 동기 — main actor 권장 SwiftUI // Heavy import Task.detached { let bgContext = ModelContext(container) for item in items { bgContext.insert(...) } try bgContext.save() } ``` ### vs Core Data ``` SwiftData: + 코드 단순, type-safe + macro 자동 + Async/await 친화 - iOS 17+ only - 일부 advanced 기능 부족 (NSPredicate 만큼) Core Data: + 안정 / 강력 + 모든 OS - Boilerplate - Objc legacy ``` → 새 프로젝트 + iOS 17+ = SwiftData. ### Performance 팁 - @Query 자동 throttle (UI 안 깨짐). - Predicate 안 무거운 계산 X. - 큰 fetch = sortBy / fetchLimit. - Background context 큰 import. ```swift let descriptor = FetchDescriptor() descriptor.fetchLimit = 50 descriptor.fetchOffset = page * 50 ``` ### Error handling ```swift do { try ctx.save() } catch { log.error("save failed: \(error)") ctx.rollback() } ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | iOS 17+ 새 프로젝트 | SwiftData | | 기존 Core Data | Core Data 유지 또는 점진 | | 매우 단순 | UserDefaults / file | | 큰 / 복잡 query | Core Data + NSFetchRequest | | Cross-platform (Android) | SQLite + SQLDelight | | Cloud sync 필요 | SwiftData + CloudKit | ## ❌ 안티패턴 - **Main thread 큰 import**: UI freeze. background context. - **@Query 너무 많은 row**: limit / pagination. - **Predicate 안 비싼 함수**: 매 row evaluation. - **save() 매 변경**: batch. - **Migration 무계획**: production data 손실. - **Core Data + SwiftData 같은 store**: 헷갈림. 한 가지. - **Schema 변경 + lightweight 가정**: 복잡 = custom migration. ## 🤖 LLM 활용 힌트 - @Model + @Query + ModelContext 3종. - SwiftUI 자동 통합 = 재렌더 자동. - Migration plan 명시. - iCloud 자동 sync — capability 켜기. ## 🔗 관련 문서 - [[iOS_Core_Data_Patterns]] - [[iOS_Swift_Macros]] - [[iOS_SwiftUI_State_Property_Wrappers]]