[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
---
|
||||
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<Note> { $0.title.contains("Foo") },
|
||||
sort: \.createdAt, order: .reverse)
|
||||
var notes: [Note]
|
||||
```
|
||||
|
||||
```swift
|
||||
// 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
|
||||
```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<Note>(
|
||||
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<Note>()
|
||||
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]]
|
||||
Reference in New Issue
Block a user