คู่มือ SwiftData ฉบับสมบูรณ์สำหรับ iOS 26: จาก @Model ถึง Model Inheritance

เรียนรู้ SwiftData ตั้งแต่พื้นฐานจนถึงขั้นสูง ครอบคลุม @Model, CRUD, @Query, Relationships, Model Inheritance ใน iOS 26 รวมถึง Schema Migration และ @ModelActor พร้อมโค้ดตัวอย่างที่ใช้ได้จริงในโปรเจกต์

SwiftData คืออะไร — ทำไมถึงเป็นอนาคตของ Data Persistence บน iOS

ถ้าคุณเคยใช้ Core Data มาก่อน คุณน่าจะเข้าใจความรู้สึกนี้ดี — การเซ็ตอัปที่ยุ่งยาก, NSManagedObjectContext ที่ต้องจัดการเอง, NSFetchRequest ที่ verbose สุดๆ แล้วก็ NSPredicate ที่ไม่ type-safe เลยแม้แต่นิดเดียว พูดตรงๆ ว่ามัน painful มากจริงๆ

SwiftData คือ framework สำหรับ data persistence ที่ Apple เปิดตัวใน WWDC 2023 มันสร้างขึ้นบน Core Data แต่ออกแบบใหม่ทั้งหมดให้ทำงานร่วมกับ Swift และ SwiftUI ได้แบบไร้รอยต่อ ไม่ต้องเขียน boilerplate code เยอะแยะอีกต่อไป ทุกอย่างทำผ่าน macro ที่กระชับและอ่านง่าย

แล้วใน iOS 26 Apple ก็ได้เพิ่มฟีเจอร์ที่นักพัฒนาเรียกร้องกันมานาน — Model Inheritance ที่ทำให้เราสร้าง subclass ของ SwiftData model ได้เป็นครั้งแรก ซึ่งเปลี่ยนวิธีการออกแบบ data model ไปอย่างสิ้นเชิง

ในบทความนี้จะพาคุณเรียนรู้ SwiftData ตั้งแต่พื้นฐานจนถึงขั้นสูง ครอบคลุมทุกหัวข้อสำคัญ:

  • การสร้าง model ด้วย @Model macro
  • การตั้งค่า ModelContainer และ ModelContext
  • CRUD operations ครบทุกรูปแบบ
  • การ query ข้อมูลด้วย @Query และ #Predicate
  • Relationships ระหว่าง model
  • การเพิ่มประสิทธิภาพด้วย #Index และ #Unique
  • Model Inheritance ฟีเจอร์ใหม่ใน iOS 26
  • Schema Migration ด้วย VersionedSchema
  • การทำงานบน Background Thread ด้วย @ModelActor

ทุกหัวข้อมาพร้อมโค้ดตัวอย่างที่ copy ไปใช้ในโปรเจกต์จริงได้เลย งั้นมาเริ่มกันเลยดีกว่า

การสร้าง Data Model ด้วย @Model Macro

หัวใจหลักของ SwiftData คือ @Model macro ซึ่งเปลี่ยน Swift class ธรรมดาๆ ให้กลายเป็น persistent model ที่จัดเก็บข้อมูลลงฐานข้อมูลได้อัตโนมัติ ไม่ต้องเขียน schema file แยก ไม่ต้องสร้าง .xcdatamodeld อีกต่อไป (ใครเคยสร้างไฟล์นี้รู้เลยว่ามันเป็นยังไง)

import SwiftData

@Model
class Task {
    var title: String
    var note: String
    var isCompleted: Bool
    var createdAt: Date

    init(title: String, note: String = "", isCompleted: Bool = false) {
        self.title = title
        self.note = note
        self.isCompleted = isCompleted
        self.createdAt = Date()
    }
}

แค่เพิ่ม @Model ไว้หน้า class เท่านี้เอง SwiftData จะจัดการทุกอย่างให้ — สร้างตาราง, ทำ change tracking, แล้วก็ทำให้ class conform กับ PersistentModel, Observable, Identifiable และ Hashable ให้โดยอัตโนมัติ

ประเภทข้อมูลที่รองรับ

SwiftData รองรับ property type ที่หลากหลายทีเดียว:

  • ประเภทพื้นฐานString, Int, Double, Bool, Date, Data, URL, UUID
  • Codable types — struct และ enum ที่ conform กับ Codable จะถูกแปลงเป็น data blob โดยอัตโนมัติ
  • Collections[String], [Int] ฯลฯ (แต่ระวังนิดนึง — array จะถูกเก็บเป็น Codable blob ทำให้ค้นหาด้วย predicate ไม่ได้)
  • Optional — property ที่เป็น optional ก็รองรับเช่นกัน

การใช้ @Attribute สำหรับ Property พิเศษ

ถ้าคุณต้องการกำหนดพฤติกรรมเพิ่มเติมให้กับ property ก็ใช้ @Attribute macro ได้แบบนี้:

@Model
class User {
    @Attribute(.unique) var email: String    // ค่าต้องไม่ซ้ำกัน
    @Attribute(.transient) var tempToken: String?  // ไม่ถูกบันทึกลง DB
    @Attribute(.externalStorage) var avatar: Data?  // เก็บเป็นไฟล์ภายนอก
    @Attribute(originalName: "user_name") var name: String  // เปลี่ยนชื่อ column

    var joinedAt: Date

    init(email: String, name: String) {
        self.email = email
        self.name = name
        self.joinedAt = Date()
    }
}

สิ่งสำคัญที่ต้องจำไว้: ทุก SwiftData model ต้องมี initializer เสมอ แม้ว่าทุก property จะมี default value แล้วก็ตาม อันนี้ลืมง่ายมาก

ModelContainer และ ModelContext — การตั้งค่าพื้นฐาน

ก่อนจะใช้ SwiftData ได้ คุณต้องเข้าใจ component หลัก 2 ตัวนี้ก่อน:

  • ModelContainer — รับผิดชอบสร้างและจัดการไฟล์ฐานข้อมูล (SQLite) จริงๆ พูดง่ายๆ ก็คือมันเป็น "คลังเก็บข้อมูล" ของแอป
  • ModelContext — เป็นตัวติดตามการเปลี่ยนแปลงทั้งหมดในหน่วยความจำ (สร้าง, แก้ไข, ลบ) ก่อนที่จะบันทึกลง container

การตั้งค่าแบบง่าย

import SwiftUI
import SwiftData

@main
struct MyTaskApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [Task.self, User.self])
        }
    }
}

เชื่อไหม แค่บรรทัดเดียว .modelContainer(for:) ระบบจะสร้าง ModelContainer และฉีด ModelContext หลัก (main context) เข้าไปใน SwiftUI environment ให้อัตโนมัติ main context นี้ทำงานบน Main Actor เสมอ จึงใช้จาก UI ได้อย่างปลอดภัย

การตั้งค่าแบบกำหนดเอง (ModelConfiguration)

ถ้าต้องการควบคุมรายละเอียดมากขึ้น เช่น กำหนดตำแหน่งไฟล์ฐานข้อมูล เปิด/ปิด CloudKit หรือตั้งค่า undo ก็ทำได้แบบนี้:

@main
struct MyTaskApp: App {
    var container: ModelContainer

    init() {
        do {
            let storeURL = URL.documentsDirectory
                .appending(path: "mytasks.sqlite")
            let config = ModelConfiguration(
                url: storeURL,
                allowsSave: true,
                cloudKitDatabase: .none
            )
            container = try ModelContainer(
                for: Task.self, User.self,
                configurations: config
            )
        } catch {
            fatalError("ไม่สามารถสร้าง ModelContainer ได้: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

CRUD Operations — สร้าง อ่าน แก้ไข ลบข้อมูล

ตั้งค่า container เสร็จแล้ว ทีนี้มาดูวิธีทำ CRUD operations กัน ตรงนี้ SwiftData ทำให้ง่ายอย่างไม่น่าเชื่อเลย

Create — สร้างข้อมูลใหม่

struct AddTaskView: View {
    @Environment(\.modelContext) private var context
    @State private var title = ""

    var body: some View {
        Form {
            TextField("ชื่อ Task", text: $title)
            Button("เพิ่ม") {
                let newTask = Task(title: title)
                context.insert(newTask)
                // ไม่ต้องเรียก save() — SwiftData auto-save ให้อัตโนมัติ
            }
        }
    }
}

จุดสำคัญที่ต้องรู้: SwiftData เปิด autosaveEnabled ไว้เป็นค่าเริ่มต้นสำหรับ main context ดังนั้นหลังจาก insert แล้วข้อมูลจะถูกบันทึกลงฐานข้อมูลให้เอง คุณไม่ต้องเรียก context.save() เลย แต่ถ้าอยากบันทึกทันทีก็เรียกได้นะ ไม่มีผลเสียอะไร

Read — อ่านข้อมูลด้วย @Query

struct TaskListView: View {
    @Query(sort: \Task.createdAt, order: .reverse)
    private var tasks: [Task]

    var body: some View {
        List(tasks) { task in
            HStack {
                Text(task.title)
                Spacer()
                if task.isCompleted {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundColor(.green)
                }
            }
        }
    }
}

@Query จะโหลดข้อมูลจากฐานข้อมูลและอัปเดต view อัตโนมัติทุกครั้งที่ข้อมูลเปลี่ยนแปลง ไม่ต้องเขียน observe หรือ listener ใดๆ ทั้งนั้น สะดวกมากจริงๆ

Update — แก้ไขข้อมูล

struct TaskDetailView: View {
    @Bindable var task: Task

    var body: some View {
        Form {
            TextField("ชื่อ", text: $task.title)
            TextField("หมายเหตุ", text: $task.note)
            Toggle("เสร็จแล้ว", isOn: $task.isCompleted)
        }
    }
}

การแก้ไขข้อมูลใน SwiftData ง่ายมากจริงๆ — แค่เปลี่ยนค่า property ของ model object โดยตรง แค่นั้นเอง SwiftData จะ track การเปลี่ยนแปลงและบันทึกให้อัตโนมัติ ไม่ต้องเรียก update method หรือ save อะไรเลย

Delete — ลบข้อมูล

struct TaskListView: View {
    @Environment(\.modelContext) private var context
    @Query private var tasks: [Task]

    var body: some View {
        List {
            ForEach(tasks) { task in
                Text(task.title)
            }
            .onDelete { indexSet in
                for index in indexSet {
                    context.delete(tasks[index])
                }
            }
        }
    }
}

@Query และ #Predicate — การกรองและค้นหาข้อมูลขั้นสูง

การ query ข้อมูลใน SwiftData ทำผ่าน @Query property wrapper ร่วมกับ #Predicate macro ซึ่งเป็น type-safe replacement ของ NSPredicate ที่ Core Data ใช้กันมานาน ข้อดีใหญ่เลยคือ compiler จะตรวจสอบ predicate ให้ตั้งแต่ตอน compile ไม่ต้องมานั่งรอ crash ตอน runtime อีกแล้ว

การกรองข้อมูลเบื้องต้น

// แสดงเฉพาะ task ที่ยังไม่เสร็จ
@Query(
    filter: #Predicate { task in
        task.isCompleted == false
    },
    sort: \Task.createdAt,
    order: .reverse
) private var pendingTasks: [Task]

การค้นหาด้วยข้อความ

// ค้นหาแบบ case-insensitive (แนะนำสำหรับ user search)
@Query(
    filter: #Predicate { task in
        task.title.localizedStandardContains("ประชุม")
    }
) private var meetingTasks: [Task]

localizedStandardContains() เป็นตัวเลือกที่ดีกว่า contains() ธรรมดาเยอะ เพราะมันไม่สนตัวเล็กตัวใหญ่ (case-insensitive) เหมาะสำหรับการค้นหาจาก input ของผู้ใช้โดยเฉพาะ

การกรองแบบ Dynamic — เปลี่ยน Filter ตามการ Input ของผู้ใช้

ปัญหาหนึ่งที่หลายคนเจอกับ @Query คือมันเป็น property wrapper ที่ต้องกำหนดค่าตอน compile time แล้วจะทำ dynamic filter ยังไงล่ะ? คำตอบคือ แยก query ไปไว้ใน subview แล้วส่งค่า filter เข้าไปตอน init:

// Parent view — รับ input จากผู้ใช้
struct SearchableTaskView: View {
    @State private var searchText = ""

    var body: some View {
        NavigationStack {
            FilteredTaskList(searchText: searchText)
                .searchable(text: $searchText)
        }
    }
}

// Child view — ใช้ @Query กับค่าที่ส่งเข้ามา
struct FilteredTaskList: View {
    @Query private var tasks: [Task]

    init(searchText: String) {
        let predicate: Predicate
        if searchText.isEmpty {
            predicate = #Predicate { _ in true }
        } else {
            predicate = #Predicate { task in
                task.title.localizedStandardContains(searchText)
            }
        }
        _tasks = Query(filter: predicate, sort: \Task.createdAt)
    }

    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }
}

เทคนิคนี้เป็น pattern มาตรฐานที่ใช้กันแพร่หลายมาก ทั้ง dynamic filter และ dynamic sort ก็ใช้หลักการเดียวกันเลย

Relationships — ความสัมพันธ์ระหว่าง Model

แอปจริงๆ แทบไม่มีทางมี model แค่ตัวเดียว เรามักต้องเชื่อมโยง model หลายตัวเข้าด้วยกัน SwiftData รองรับ relationships ทั้งแบบ one-to-one, one-to-many และ many-to-many

ตัวอย่าง: Project กับ Task (One-to-Many)

@Model
class Project {
    var name: String
    var color: String

    @Relationship(deleteRule: .cascade, inverse: \Task.project)
    var tasks: [Task]

    init(name: String, color: String = "blue") {
        self.name = name
        self.color = color
        self.tasks = []
    }
}

@Model
class Task {
    var title: String
    var isCompleted: Bool
    var project: Project?

    init(title: String, isCompleted: Bool = false) {
        self.title = title
        self.isCompleted = isCompleted
    }
}

Delete Rules ที่ต้องรู้

ตรงนี้สำคัญมากนะ เลือกผิดอาจ crash ได้:

  • .nullify (ค่าเริ่มต้น) — เมื่อลบ parent, property ของ child ที่อ้างถึง parent จะถูกตั้งเป็น nil ระวังให้ดี! ถ้า property ไม่ใช่ optional จะ crash ทันที
  • .cascade — ลบ parent แล้ว child ทั้งหมดจะถูกลบตามไปด้วย เหมาะสำหรับ relationship ที่ child ไม่มีความหมายถ้าไม่มี parent
  • .deny — ห้ามลบ parent ถ้ายังมี child อยู่
  • .noAction — ไม่ทำอะไรเลย (ใช้ด้วยความระมัดระวังจริงๆ)

เคล็ดลับสำคัญ: ใช้ @Relationship (พร้อม inverse:) ด้านเดียวเท่านั้น ถ้าใส่ทั้งสองด้าน Swift จะ compile ไม่ผ่าน เพราะทั้งสองฝั่งอ้างอิงกันวนลูป เรื่องนี้หลายคนเคยเจอแล้วงงว่าทำไม error

การเพิ่ม Task ให้กับ Project

let project = Project(name: "แอปจัดการงาน")
let task1 = Task(title: "ออกแบบ UI")
let task2 = Task(title: "เขียน API")

task1.project = project
task2.project = project

context.insert(project)
// ไม่ต้อง insert task1, task2 แยก — SwiftData จะ insert ให้อัตโนมัติ
// ⚠️ ถ้า insert ซ้ำจะเจอ error "Duplicate registration attempt"

การเรียงลำดับข้อมูล — Static Sort และ Dynamic Sort

Static Sort

// เรียงตาม field เดียว
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]

// เรียงหลาย field — ใช้ SortDescriptor array
@Query(sort: [
    SortDescriptor(\Task.isCompleted),
    SortDescriptor(\Task.createdAt, order: .reverse)
]) private var tasks: [Task]

Dynamic Sort — ให้ผู้ใช้เลือกวิธีเรียง

struct TaskContainerView: View {
    @State private var sortOrder = SortDescriptor(\Task.createdAt, order: .reverse)

    var body: some View {
        NavigationStack {
            SortedTaskList(sort: sortOrder)
                .toolbar {
                    Menu("เรียง") {
                        Button("วันที่สร้าง") {
                            sortOrder = SortDescriptor(\Task.createdAt, order: .reverse)
                        }
                        Button("ชื่อ") {
                            sortOrder = SortDescriptor(\Task.title)
                        }
                    }
                }
        }
    }
}

struct SortedTaskList: View {
    @Query private var tasks: [Task]

    init(sort: SortDescriptor) {
        _tasks = Query(sort: [sort])
    }

    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }
}

#Index และ #Unique — เพิ่มประสิทธิภาพและป้องกันข้อมูลซ้ำ

เมื่อแอปมีข้อมูลเยอะขึ้น การค้นหาและ filter จะเริ่มช้าลงอย่างเห็นได้ชัด นี่คือจุดที่ #Index และ #Unique macro (เปิดตัวใน WWDC 2024) เข้ามาช่วยได้

#Index — สร้าง Index สำหรับ Query ที่เร็วขึ้น

@Model
class Article {
    #Index
([\.title], [\.publishedAt], [\.title, \.publishedAt]) var title: String var content: String var publishedAt: Date var isPublished: Bool init(title: String, content: String, publishedAt: Date = .now) { self.title = title self.content = content self.publishedAt = publishedAt self.isPublished = false } }

#Index สร้าง binary index ในฐานข้อมูล SQLite ทำให้การค้นหาจากเดิมที่ต้อง scan ทุก row (linear search) เปลี่ยนเป็น binary search ที่เร็วกว่ามากเมื่อข้อมูลมีจำนวนเยอะ สร้างได้ทั้ง single index และ compound index (หลาย field รวมกัน)

#Unique — ป้องกันข้อมูลซ้ำ

@Model
class Event {
    #Unique([\.name, \.date])

    var name: String
    var date: Date
    var location: String

    init(name: String, date: Date, location: String) {
        self.name = name
        self.date = date
        self.location = location
    }
}

#Unique กำหนดว่า combination ของ property ไหนบ้างที่ต้องไม่ซ้ำกัน ถ้า insert object ที่ unique values ซ้ำกับ object ที่มีอยู่แล้ว SwiftData จะทำ upsert (อัปเดตข้อมูลเดิมแทนที่จะสร้างใหม่) ให้อัตโนมัติ ซึ่งสะดวกมากสำหรับการ sync ข้อมูลจาก API

ข้อควรระวังหนึ่ง: ทั้ง @Attribute(.unique) และ #Unique ไม่รองรับ CloudKit ถ้าแอปของคุณใช้ CloudKit sync จะโหลด model container ไม่ได้เลย ตรงนี้ต้องระวัง

Model Inheritance ใน iOS 26 — ฟีเจอร์ใหม่ที่เปลี่ยนเกม

ก่อน iOS 26 SwiftData model ไม่สามารถสืบทอด (inherit) จากกันได้เลย ถ้าคุณมี model ที่คล้ายกันหลายตัว ก็ต้อง copy property ซ้ำไปทุกตัว ซึ่งผิดหลัก DRY (Don't Repeat Yourself) อย่างแรง

พอมาถึง iOS 26 Apple ก็แก้ปัญหานี้ด้วยการเพิ่ม class inheritance ให้กับ SwiftData ทำให้สร้าง subclass ที่สืบทอด property และ behavior จาก parent model ได้แล้ว ต้องบอกว่ารอฟีเจอร์นี้มานานเหมือนกัน

ตัวอย่าง: ระบบ Trip ที่มีหลายประเภท

// Base Model — property ที่ทุก Trip ต้องมี
@Model
class Trip {
    var destination: String
    var startDate: Date
    var endDate: Date
    var budget: Decimal

    init(destination: String, startDate: Date, endDate: Date, budget: Decimal) {
        self.destination = destination
        self.startDate = startDate
        self.endDate = endDate
        self.budget = budget
    }
}

// Subclass สำหรับ Business Trip
@available(iOS 26, *)
class BusinessTrip: Trip {
    var companyName: String
    var perDiem: Double

    init(destination: String, startDate: Date, endDate: Date,
         budget: Decimal, companyName: String, perDiem: Double) {
        self.companyName = companyName
        self.perDiem = perDiem
        super.init(destination: destination, startDate: startDate,
                   endDate: endDate, budget: budget)
    }
}

// Subclass สำหรับ Personal Trip
@available(iOS 26, *)
class PersonalTrip: Trip {
    enum Reason: String, Codable {
        case vacation, family, adventure
    }
    var reason: Reason

    init(destination: String, startDate: Date, endDate: Date,
         budget: Decimal, reason: Reason) {
        self.reason = reason
        super.init(destination: destination, startDate: startDate,
                   endDate: endDate, budget: budget)
    }
}

สิ่งสำคัญที่ต้องจำ:

  • Subclass ต้องใส่ @available(iOS 26, *) เสมอ
  • ไม่ต้องใส่ @Model ซ้ำที่ subclass — มันสืบทอดมาจาก parent แล้ว
  • ต้องเพิ่ม subclass ทุกตัวลงใน modelContainer ด้วย อย่าลืม!

การลงทะเบียน Subclass ใน ModelContainer

@main
struct TripApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [
                    Trip.self,
                    BusinessTrip.self,
                    PersonalTrip.self
                ])
        }
    }
}

การ Query ตามประเภท Subclass

SwiftData รองรับการใช้ is keyword ใน predicate สำหรับ type checking ซึ่งอ่านง่ายมาก:

// Query เฉพาะ BusinessTrip
@Query(filter: #Predicate { trip in
    trip is BusinessTrip
}) private var businessTrips: [Trip]

// Query เฉพาะ PersonalTrip
@Query(filter: #Predicate { trip in
    trip is PersonalTrip
}) private var personalTrips: [Trip]

Schema Migration — จัดการเวอร์ชันฐานข้อมูลอย่างปลอดภัย

ทุกครั้งที่คุณเปลี่ยนแปลง SwiftData model (เพิ่ม property, ลบ property, เปลี่ยนชื่อ ฯลฯ) คุณต้องบอก SwiftData ว่าจะ migrate ข้อมูลเก่าไปสู่ schema ใหม่ยังไง ไม่งั้นข้อมูลอาจสูญหายหรือแอป crash ได้

Step 1: สร้าง VersionedSchema สำหรับทุกเวอร์ชัน

// เวอร์ชันแรก
enum TaskSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }

    @Model class Task {
        var title: String
        var isDone: Bool
        init(title: String, isDone: Bool = false) {
            self.title = title
            self.isDone = isDone
        }
    }
}

// เวอร์ชัน 2 — เพิ่ม priority และเปลี่ยนชื่อ isDone เป็น isCompleted
enum TaskSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }

    @Model class Task {
        var title: String
        @Attribute(originalName: "isDone") var isCompleted: Bool
        var priority: Int
        init(title: String, isCompleted: Bool = false, priority: Int = 0) {
            self.title = title
            self.isCompleted = isCompleted
            self.priority = priority
        }
    }
}

Step 2: สร้าง SchemaMigrationPlan

enum TaskMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TaskSchemaV1.self, TaskSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    // Lightweight migration — SwiftData จัดการให้อัตโนมัติ
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: TaskSchemaV1.self,
        toVersion: TaskSchemaV2.self
    )
}

Step 3: ใช้ Migration Plan กับ ModelContainer

let container = try ModelContainer(
    for: Schema(versionedSchema: TaskSchemaV2.self),
    migrationPlan: TaskMigrationPlan.self
)

Custom Migration — สำหรับการเปลี่ยนแปลงที่ซับซ้อน

ถ้า lightweight migration ไม่เพียงพอ เช่น ต้อง deduplicate ข้อมูล หรือแปลงค่าจาก format หนึ่งไปอีก format หนึ่ง ก็ใช้ custom migration ได้:

static let migrateV2toV3 = MigrationStage.custom(
    fromVersion: TaskSchemaV2.self,
    toVersion: TaskSchemaV3.self,
    willMigrate: { context in
        // ลบข้อมูลซ้ำก่อน migrate
        let tasks = try context.fetch(FetchDescriptor())
        var seenTitles = Set()
        for task in tasks {
            if seenTitles.contains(task.title) {
                context.delete(task)
            } else {
                seenTitles.insert(task.title)
            }
        }
        try context.save()
    },
    didMigrate: nil
)

คำแนะนำจากประสบการณ์: เริ่มใช้ VersionedSchema ตั้งแต่เวอร์ชันแรกของแอปเลย อย่ารอจนมีปัญหาค่อยทำ เพราะถ้าไม่มี versioned schema ไว้ตั้งแต่แรก การ migrate ในภายหลังจะยุ่งยากมากจริงๆ

@ModelActor — การทำงานกับ SwiftData บน Background Thread

สำหรับงานหนักๆ อย่างการ import ข้อมูลจำนวนมาก หรือ sync ข้อมูลจาก server คุณไม่ควรทำบน main thread เด็ดขาด เพราะจะทำให้ UI ค้าง SwiftData มี @ModelActor macro มาให้สำหรับเรื่องนี้โดยเฉพาะ

สิ่งที่ต้องรู้ก่อน

ตรงนี้สำคัญมาก ต้องเข้าใจก่อนเริ่มเขียน:

  • SwiftData model ไม่ใช่ Sendable — ส่งข้าม actor ไม่ได้โดยตรง
  • ModelContext ก็ไม่ใช่ Sendable เช่นกัน — ห้ามแชร์ข้าม thread
  • แต่ ModelContainer เป็น Sendable — ส่งข้าม actor ได้อย่างปลอดภัย
  • PersistentIdentifier ก็เป็น Sendable — ใช้ส่ง ID ข้าม actor แทน model object ได้

ตัวอย่าง: สร้าง Background Actor สำหรับ Import ข้อมูล

import SwiftData

@ModelActor
actor DataImporter {

    func importTasks(from data: [TaskDTO]) throws -> Int {
        var importedCount = 0

        for dto in data {
            let task = Task(title: dto.title, note: dto.note)
            task.isCompleted = dto.isCompleted
            modelContext.insert(task)
            importedCount += 1
        }

        try modelContext.save()
        return importedCount
    }

    func deleteAllCompleted() throws {
        let predicate = #Predicate { $0.isCompleted == true }
        try modelContext.delete(model: Task.self, where: predicate)
        try modelContext.save()
    }
}

// การใช้งานใน View
struct ImportView: View {
    @Environment(\.modelContext) private var context

    var body: some View {
        Button("Import ข้อมูล") {
            Task {
                let importer = DataImporter(
                    modelContainer: context.container
                )
                let count = try await importer.importTasks(from: sampleData)
                print("Import สำเร็จ \(count) รายการ")
            }
        }
    }
}

@ModelActor จะสร้าง ModelContext ของตัวเองบน background thread โดยอัตโนมัติ แล้วใช้ DefaultSerialModelExecutor เพื่อทำให้ทุก operation ทำงานแบบ serial (ทีละอัน) ป้องกัน data race ได้อย่างปลอดภัย

เคล็ดลับ: ถ้าต้องส่งข้อมูลระหว่าง @ModelActor กับ main actor ให้แปลง model เป็น plain struct (Sendable) ก่อน หรือใช้ PersistentIdentifier แทนการส่ง model object โดยตรง วิธีหลังนี้เป็นที่นิยมกว่าเพราะเขียนง่ายกว่า

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

จากประสบการณ์ของนักพัฒนาที่ใช้ SwiftData มาสักพัก นี่คือข้อผิดพลาดที่เจอกันบ่อยที่สุด:

1. "Fatal error: Duplicate registration attempt"

สาเหตุ: insert ทั้ง parent และ child object แยกกัน ทั้งที่ SwiftData insert relationship ให้อัตโนมัติแล้ว

แก้ไข: insert แค่ parent ตัวเดียวก็พอ SwiftData จะ insert child ที่เชื่อมโยงให้เอง

2. Crash เมื่อลบ parent ที่มี non-optional relationship

สาเหตุ: delete rule เป็น .nullify (ค่าเริ่มต้น) แต่ property ของ child ไม่ใช่ optional

แก้ไข: เปลี่ยน delete rule เป็น .cascade หรือทำ property ให้เป็น optional

3. Array ใน Predicate ค้นหาไม่ได้

สาเหตุ: property ที่เป็น array (เช่น [String]) ถูกเก็บเป็น Codable blob ซึ่ง SQLite ค้นหาภายในไม่ได้

แก้ไข: สร้าง model แยกและใช้ relationship แทน แม้ว่า model นั้นจะมีแค่ property เดียวก็ตาม ฟังดูเกินไปนิดแต่เป็นวิธีที่ถูกต้อง

4. EXC_BAD_ACCESS เมื่อใช้ model ข้าม thread

สาเหตุ: ส่ง SwiftData model object ข้าม actor/thread ซึ่ง model ไม่ใช่ Sendable

แก้ไข: ใช้ @ModelActor สำหรับ background work และส่ง PersistentIdentifier แทน model object

5. ลำดับ array ใน relationship สลับทุกครั้งที่โหลด

สาเหตุ: SwiftData ไม่ได้เก็บลำดับของ array ใน relationship ลงฐานข้อมูล

แก้ไข: เพิ่ม property สำหรับ sort order (เช่น sortIndex: Int) แล้ว sort ด้วยตัวเองตอนแสดงผล

คำถามที่พบบ่อย (FAQ)

SwiftData กับ Core Data ต่างกันยังไง? ควรใช้ตัวไหน?

SwiftData สร้างขึ้นบน Core Data แต่มี API ที่ทันสมัยกว่ามาก ใช้ Swift macro, type-safe predicate, และรวมกับ SwiftUI ได้อย่างลงตัว ถ้าเริ่มโปรเจกต์ใหม่แนะนำ SwiftData เลย ไม่ต้องคิดเยอะ แต่ถ้าโปรเจกต์เดิมใช้ Core Data อยู่แล้วและทำงานได้ดี ก็ไม่จำเป็นต้องรีบ migrate สามารถรันทั้งสองตัวพร้อมกันในแอปเดียวกันได้

SwiftData รองรับ CloudKit ไหม?

รองรับครับ SwiftData ทำงานร่วมกับ CloudKit ได้ผ่าน ModelConfiguration เพียงตั้งค่า cloudKitDatabase parameter แต่ต้องระวังว่าฟีเจอร์บางอย่างอย่าง @Attribute(.unique) และ #Unique ใช้กับ CloudKit ไม่ได้ แถม CloudKit ยังจำกัดอยู่แค่ private database เท่านั้น (ยังไม่รองรับ shared/public database)

SwiftData ใช้กับ struct ได้ไหม?

@Model macro ใช้ได้กับ class เท่านั้น ไม่รองรับ struct โดยตรง แต่คุณสามารถใช้ struct และ enum ที่ conform กับ Codable เป็น property ภายใน model class ได้ SwiftData จะแปลงเป็น data blob ให้อัตโนมัติ

ต้องเรียก save() เองไหม?

ปกติไม่ต้องเลย main context ของ SwiftData เปิด autosaveEnabled เป็นค่าเริ่มต้น ระบบจะบันทึกให้อัตโนมัติเมื่อถึงจังหวะที่เหมาะสม แต่ถ้าทำงานใน @ModelActor บน background thread ตรงนี้ควรเรียก save() เองเพื่อให้แน่ใจว่าข้อมูลถูกบันทึกก่อนที่ actor จะจบ

Model Inheritance ของ iOS 26 ใช้กับ iOS เวอร์ชันเก่าได้ไหม?

ไม่ได้ครับ Model Inheritance เป็นฟีเจอร์ที่ต้องใช้ iOS 26 ขึ้นไปเท่านั้น ถ้าแอปของคุณยัง support iOS เวอร์ชันเก่ากว่า จะใช้ inheritance ไม่ได้ ต้องใช้วิธีอื่นแทน เช่น composition (ฝัง model หนึ่งไว้ใน property ของอีก model) หรือ protocol ก็เป็นทางเลือกที่ดี

เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.