๐พ SwiftData ์์ ์ ๋ณต
Core Data๋ฅผ ๋์ฒดํ๋ ํ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค. Swift ๋งคํฌ๋ก๋ก ๊ฐํธํ๊ฒ ์๊ตฌ ์ ์ฅ์๋ฅผ ๊ตฌํํฉ๋๋ค.
โจ SwiftData๋?
SwiftData๋ iOS 17+์์ ์ฌ์ฉ ๊ฐ๋ฅํ ์๋ก์ด ๋ฐ์ดํฐ ์์ํ ํ๋ ์์ํฌ์ ๋๋ค. Core Data๋ฅผ ์์ ํ ๋์ฒดํ๋ฉฐ, Swift ๋งคํฌ๋ก๋ฅผ ํ์ฉํด ํจ์ฌ ๊ฐ๋จํฉ๋๋ค.
๐ฆ Model ์ ์
Todo.swift
import SwiftData import Foundation @Model final class Todo { var title: String var isCompleted: Bool var createdAt: Date init(title: String, isCompleted: Bool = false) { self.title = title self.isCompleted = isCompleted self.createdAt = Date() } }
๐๏ธ App ์ค์
TodoApp.swift
import SwiftUI import SwiftData @main struct TodoApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Todo.self) } }
๐ ๋ฐ์ดํฐ ์ฝ๊ธฐ (@Query)
ContentView.swift
import SwiftUI import SwiftData struct ContentView: View { @Query private var todos: [Todo] @Environment(\.modelContext) private var modelContext var body: some View { NavigationStack { List { ForEach(todos) { todo in HStack { Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundStyle(todo.isCompleted ? .green : .gray) Text(todo.title) .strikethrough(todo.isCompleted) } .onTapGesture { todo.isCompleted.toggle() } } .onDelete(perform: deleteTodos) } .navigationTitle("ํ ์ผ") .toolbar { Button("์ถ๊ฐ", systemImage: "plus") { addTodo() } } } } func addTodo() { let newTodo = Todo(title: "์ ํ ์ผ") modelContext.insert(newTodo) } func deleteTodos(at offsets: IndexSet) { for index in offsets { modelContext.delete(todos[index]) } } }
๐ Query ํํฐ๋ง & ์ ๋ ฌ
FilteredView.swift
struct FilteredTodoView: View { // ์๋ฃ๋์ง ์์ ํ ์ผ๋ง, ์ต์ ์ ์ ๋ ฌ @Query( filter: #Predicate<Todo> { todo in !todo.isCompleted }, sort: \Todo.createdAt, order: .reverse ) private var incompleteTodos: [Todo] var body: some View { List(incompleteTodos) { todo in Text(todo.title) } } } // ์ฌ๋ฌ ์ ๋ ฌ ์กฐ๊ฑด @Query(sort: [ SortDescriptor(\Todo.isCompleted), SortDescriptor(\Todo.createdAt, order: .reverse) ]) private var sortedTodos: [Todo]
๐ ๊ด๊ณ(Relationship) ์ ์
Models.swift
import SwiftData @Model final class Category { var name: String @Relationship(deleteRule: .cascade, inverse: \Todo.category) var todos: [Todo] = [] init(name: String) { self.name = name } } @Model final class Todo { var title: String var isCompleted: Bool var category: Category? init(title: String, category: Category? = nil) { self.title = title self.isCompleted = false self.category = category } }
๐พ ์๋ ์ ์ฅ & ๋กค๋ฐฑ
ManualSave.swift
struct TodoEditView: View { @Environment(\.modelContext) private var modelContext @State private var title = "" var body: some View { Form { TextField("์ ๋ชฉ", text: $title) HStack { Button("์ ์ฅ") { save() } Button("์ทจ์", role: .cancel) { rollback() } } } } func save() { let todo = Todo(title: title) modelContext.insert(todo) do { try modelContext.save() } catch { print("์ ์ฅ ์คํจ: \(error)") } } func rollback() { modelContext.rollback() } }
๐ง ModelContext ์ง์ ์ฌ์ฉ
FetchData.swift
import SwiftData func fetchTodos(context: ModelContext) throws -> [Todo] { let descriptor = FetchDescriptor<Todo>( predicate: #Predicate { !$0.isCompleted }, sortBy: [SortDescriptor(\Todo.createdAt, order: .reverse)] ) return try context.fetch(descriptor) } // ๊ฐ์๋ง ๊ฐ์ ธ์ค๊ธฐ func countTodos(context: ModelContext) throws -> Int { let descriptor = FetchDescriptor<Todo>() return try context.fetchCount(descriptor) }
๐๏ธ ์ญ์ ๊ท์น (Delete Rule)
DeleteRule.swift
@Model final class Project { var name: String // cascade: Project ์ญ์ ์ Task๋ ๋ชจ๋ ์ญ์ @Relationship(deleteRule: .cascade) var tasks: [Task] = [] // nullify: Project ์ญ์ ์ Task์ project๋ฅผ nil๋ก // @Relationship(deleteRule: .nullify) // deny: Task๊ฐ ์์ผ๋ฉด Project ์ญ์ ๋ถ๊ฐ // @Relationship(deleteRule: .deny) init(name: String) { self.name = name } } @Model final class Task { var title: String var project: Project? init(title: String, project: Project? = nil) { self.title = title self.project = project } }
๐ฏ Unique ์ ์ฝ ์กฐ๊ฑด
UniqueModel.swift
import SwiftData @Model final class User { @Attribute(.unique) var email: String var name: String init(email: String, name: String) { self.email = email self.name = name } } // ๋์ผํ email๋ก insert ์ ์๋์ผ๋ก update๋จ
๐งช ํ ์คํธ์ฉ In-Memory Container
PreviewContainer.swift
import SwiftData extension ModelContainer { static var preview: ModelContainer { let schema = Schema([Todo.self]) let configuration = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: schema, configurations: configuration) // ์ํ ๋ฐ์ดํฐ ์ถ๊ฐ let context = container.mainContext context.insert(Todo(title: "ํ ์ผ 1")) context.insert(Todo(title: "ํ ์ผ 2")) return container } } // Preview์์ ์ฌ์ฉ #Preview { ContentView() .modelContainer(ModelContainer.preview) }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
โ
@Query๋ ์๋์ผ๋ก UI ์
๋ฐ์ดํธ
โ
ModelContext๋ ์๋ ์ ์ฅ
โ
Core Data๋ณด๋ค 80% ์ ์ ์ฝ๋
โ
ํ์
์์ ์ฑ ๋ณด์ฅ
โ
Swift ๋งคํฌ๋ก๋ก ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ ๊ฑฐ