Files
2nd/10_Wiki/Topics/Coding/iOS_SwiftData_Patterns.md
T
2026-05-09 21:08:02 +09:00

6.2 KiB


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

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

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Note.self, Tag.self])
    }
}

SwiftUI — @Query

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)

@Query(filter: #Predicate<Note> { $0.title.contains("Foo") },
       sort: \.createdAt, order: .reverse)
var notes: [Note]
// Dynamic
@Query private var notes: [Note]

init(searchText: String) {
    let predicate = #Predicate<Note> { $0.title.localizedStandardContains(searchText) }
    _notes = Query(filter: predicate, sort: \.createdAt, order: .reverse)
}

CRUD

// 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<Note>(
    predicate: #Predicate { $0.title.contains("Foo") },
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
let notes = try ctx.fetch(descriptor)

Relationship

let note = Note(title: "...", body: "...")
let tag = Tag(name: "important")
note.tags.append(tag)
ctx.insert(note)
try ctx.save()
// tag 도 자동 insert

Cascade delete

@Relationship(deleteRule: .cascade) var tags: [Tag]
// note 삭제 = tags 도

@Relationship(deleteRule: .nullify) var author: User?
// note 삭제 = user.notes 에서 nullify

iCloud sync (CloudKit)

.modelContainer(for: Note.self, isAutosaveEnabled: true)

// Cloud 자동 sync — Bundle ID + iCloud capability + entitlement
Capabilities → iCloud → CloudKit
+ Background Modes → Remote notifications

→ 사용자가 iCloud 계정만 있으면 자동.

Migration

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

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.
let descriptor = FetchDescriptor<Note>()
descriptor.fetchLimit = 50
descriptor.fetchOffset = page * 50

Error handling

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 켜기.

🔗 관련 문서