๐Ÿ’พ 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 ๋งคํฌ๋กœ๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ œ๊ฑฐ

๐Ÿ“ฆ ํ•™์Šต ์ž๋ฃŒ

๐Ÿ’ป
GitHub ํ”„๋กœ์ ํŠธ
๐ŸŽ
Apple ๊ณต์‹ ๋ฌธ์„œ
๐ŸŽฅ
WWDC23 ์„ธ์…˜