๐ŸŒ KO

๐Ÿ’พ Mastering SwiftData

A modern database replacing Core Data. Easily implement persistent storage with Swift macros.

โญ Difficulty: โญโญ โฑ๏ธ Est. Time: 1-2h ๐Ÿ“‚ App Frameworks

โœจ SwiftData?

SwiftData is a new data persistence framework available on iOS 17+. It completely replaces Core Data and is much simpler thanks to Swift macros.

๐Ÿ“ฆ Define 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 Setup

TodoApp.swift
import SwiftUI
import SwiftData

@main
struct TodoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Todo.self)
    }
}

๐Ÿ“ Reading Data (@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("Add", 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 Filtering & Sorting

FilteredView.swift
struct FilteredTodoView: View {
    // Complete๋˜์ง€ ์•Š์€ ํ•  ์ผ๋งŒ, ์ตœ์‹ ์ˆœ ์ •๋ ฌ
    @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]

๐Ÿ”— Defining Relationships

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
    }
}

๐Ÿ’พ Manual Save & Rollback

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") {
                    save()
                }

                Button("Cancel", role: .cancel) {
                    rollback()
                }
            }
        }
    }

    func save() {
        let todo = Todo(title: title)
        modelContext.insert(todo)

        do {
            try modelContext.save()
        } catch {
            print("Save failed: \(error)")
        }
    }

    func rollback() {
        modelContext.rollback()
    }
}

๐Ÿ”ง Direct ModelContext Usage

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)
}

// ๊ฐœ์ˆ˜๋งŒ Fetch
func countTodos(context: ModelContext) throws -> Int {
    let descriptor = FetchDescriptor<Todo>()
    return try context.fetchCount(descriptor)
}

๐Ÿ—‘๏ธ Delete Rules

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 Constraints

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 ์‹œ Automatically update๋จ

๐Ÿงช In-Memory Container for Testing

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)

        // Sample ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
        let context = container.mainContext
        context.insert(Todo(title: "ํ•  ์ผ 1"))
        context.insert(Todo(title: "ํ•  ์ผ 2"))

        return container
    }
}

// Preview์—์„œ ์‚ฌ์šฉ
#Preview {
    ContentView()
        .modelContainer(ModelContainer.preview)
}

๐Ÿ’ก HIG Guidelines
โœ… @Query๋Š” Automatically UI ์—…๋ฐ์ดํŠธ
โœ… ModelContext๋Š” auto save
โœ… Core Data๋ณด๋‹ค 80% ์ ์€ ์ฝ”๋“œ
โœ… ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ
โœ… Swift ๋งคํฌ๋กœ๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ œ๊ฑฐ

๐Ÿ“ฆ Learning Resources

๐Ÿ’ป
GitHub Project
๐ŸŽ
Apple Official Docs
๐ŸŽฅ
WWDC23 Session

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions