SwiftUI @Observable vs @State: ใช้ตัวไหนเมื่อไหร่ในปี 2026
เข้าใจความแตกต่างระหว่าง @Observable และ @State ใน SwiftUI เพื่อเลือกเครื่องมือจัดการ state ที่เหมาะกับแอป iOS

การจัดการ state คือรากฐานของแอป SwiftUI ที่มีประสิทธิภาพ ตั้งแต่ iOS 17 เป็นต้นมา macro @Observable ได้พลิกโฉมการสร้างโมเดลแบบ reactive ในขณะที่ @State ยังคงสำคัญสำหรับ state ภายในของ view การเข้าใจว่าควรใช้เครื่องมือใดเมื่อไรช่วยลดการ re-render ที่ไม่จำเป็นและทำให้สร้างแอปที่ลื่นไหลและตอบสนองได้ดี
บทความนี้สำรวจกลไกภายในของ @Observable และ @State ความแตกต่างพื้นฐาน และแนวทางที่ชัดเจนในการเลือกเครื่องมือที่เหมาะสมตามบริบท รวมถึงการเปลี่ยนแปลง macro @State ใน iOS 27 (backport ไปยัง iOS 17)
พื้นฐาน @State
@State คือรูปแบบการจัดการ state ที่ง่ายที่สุดใน SwiftUI property wrapper นี้สร้างที่จัดเก็บข้อมูลถาวรสำหรับค่าที่เป็นของ view ที่ประกาศมันโดยเฉพาะ
struct CounterView: View {
// @State creates storage managed by SwiftUI
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
// The view updates when count changes
Text("Counter: (count)")
.font(.largeTitle)
HStack(spacing: 16) {
Button("- 1") {
count -= 1
}
Button("+ 1") {
count += 1
}
}
.buttonStyle(.borderedProminent)
}
}
}ทุกการแก้ไข count จะกระตุ้นให้ view ถูก render ใหม่ SwiftUI จัดการวงจรชีวิตของค่านี้โดยอัตโนมัติ และเก็บรักษาไว้ระหว่างการสร้าง body ใหม่
คุณสมบัติสำคัญของ @State
@State มีคุณสมบัติเด่นหลายประการที่กำหนดการใช้งานที่เหมาะสม
struct FormView: View {
// Simple local state with value types
@State private var username = ""
@State private var isEnabled = true
@State private var selectedIndex = 0
// Complex value types supported
@State private var configuration = FormConfiguration()
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Enabled", isOn: $isEnabled)
Picker("Option", selection: $selectedIndex) {
Text("Option A").tag(0)
Text("Option B").tag(1)
Text("Option C").tag(2)
}
}
}
}
// Structs work perfectly with @State
struct FormConfiguration: Equatable {
var theme: Theme = .light
var fontSize: CGFloat = 16
var showNotifications: Bool = true
}
enum Theme {
case light, dark, system
}จุดสำคัญคือ @State ทำงานกับ value type (struct, enum, ชนิดพื้นฐาน) สำหรับ reference type (class) ต้องใช้เครื่องมืออื่น
Macro @Observable อธิบาย
เปิดตัวพร้อม iOS 17 @Observable แปลงคลาสใดก็ตามให้เป็นแหล่งข้อมูลแบบ reactive ต่างจาก protocol ObservableObject แบบเดิม macro นี้ให้การสังเกตการณ์ที่ละเอียดอ่อน เฉพาะ property ที่ view อ่านจริงเท่านั้นที่จะกระตุ้นการ re-render เอกสาร Observation framework ให้ข้อมูลอ้างอิง API ที่ครบถ้วน
import Observation
// @Observable transforms the class into a reactive source
@Observable
class UserModel {
var name: String = ""
var email: String = ""
var avatarURL: URL?
var preferences = UserPreferences()
// Computed properties work too
var isValid: Bool {
!name.isEmpty && email.contains("@")
}
}
struct UserPreferences {
var newsletter: Bool = false
var notifications: Bool = true
var theme: Theme = .system
}ความมหัศจรรย์เกิดขึ้นในเวลาคอมไพล์ macro สร้างโค้ดติดตามที่จำเป็นสำหรับแต่ละ property โดยอัตโนมัติ
การสังเกตแบบละเอียดในการใช้งานจริง
ความแตกต่างหลักจาก ObservableObject แบบเก่าอยู่ที่ความละเอียดของการติดตาม
@Observable
class ProfileModel {
var name: String = ""
var bio: String = ""
var followerCount: Int = 0
var posts: [Post] = []
}
struct ProfileHeaderView: View {
let model: ProfileModel
var body: some View {
VStack {
// This view only re-renders if name or bio change
Text(model.name)
.font(.title)
Text(model.bio)
.foregroundStyle(.secondary)
}
}
}
struct FollowerCountView: View {
let model: ProfileModel
var body: some View {
// This view only re-renders if followerCount changes
HStack {
Image(systemName: "person.2")
Text("(model.followerCount) followers")
}
}
}
struct ProfileScreen: View {
@State private var model = ProfileModel()
var body: some View {
VStack {
// Each subview tracks only its dependencies
ProfileHeaderView(model: model)
FollowerCountView(model: model)
Button("Simulate new follower") {
// Only re-renders FollowerCountView
model.followerCount += 1
}
}
}
}SwiftUI วิเคราะห์ body ของแต่ละ view เพื่อระบุว่ามี property ใดถูกอ่าน เฉพาะ property เหล่านั้นเท่านั้นที่จะกระตุ้นการ re-render เมื่อมีการเปลี่ยนแปลง
เปรียบเทียบโดยตรง: @Observable vs @State
การเลือกระหว่างสองเครื่องมือนี้ขึ้นอยู่กับหลายปัจจัย ต่อไปนี้คือการเปรียบเทียบที่มีโครงสร้าง
// Scenario 1: Temporary UI state -> @State
struct ToggleExample: View {
@State private var isExpanded = false // @State appropriate
var body: some View {
VStack {
Button(isExpanded ? "Collapse" : "Expand") {
withAnimation {
isExpanded.toggle()
}
}
if isExpanded {
Text("Detailed content...")
}
}
}
}
// Scenario 2: Shared business data -> @Observable
@Observable
class CartModel { // @Observable appropriate
var items: [CartItem] = []
var promoCode: String?
var total: Decimal {
items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
}
var itemCount: Int {
items.reduce(0) { $0 + $1.quantity }
}
func addItem(_ item: CartItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].quantity += 1
} else {
items.append(item)
}
}
func removeItem(_ item: CartItem) {
items.removeAll { $0.id == item.id }
}
}
struct CartItem: Identifiable, Equatable {
let id: UUID
let name: String
let price: Decimal
var quantity: Int
}ตารางสรุปกรณีการใช้งาน
| เกณฑ์ | @State | @Observable |
|---|---|---|
| ประเภทข้อมูล | Value type (struct, enum) | Class |
| ขอบเขต | ภายใน view เดียว | แชร์ระหว่าง view ได้ |
| ความซับซ้อน | State แบบง่าย | ลอจิกธุรกิจที่ซับซ้อน |
| วงจรชีวิต | จัดการโดย SwiftUI | จัดการอย่างชัดเจน |
| Re-render | ทั้ง view | ละเอียดต่อ property |
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
อัปเดต iOS 27: @State เป็น macro พร้อมการ initialize แบบ lazy
WWDC 2026 เปิดตัวการเปลี่ยนแปลงสำคัญกับ @State: ตอนนี้ถูก implement เป็น macro แทนที่จะเป็น property wrapper การเปลี่ยนแปลงนี้แก้ไขปัญหาที่พบบ่อยเมื่อใช้ @State กับคลาส @Observable โดยตรง
ปัญหาก่อน iOS 27
ก่อนหน้านี้ คลาส @Observable ที่เก็บใน @State ถูก initialize แบบ eager ซึ่งอาจทำให้ instance เดียวกันถูกสร้างหลายครั้งก่อนที่ view จะแสดง:
// Before iOS 27: potential multiple initializations
struct StickerStoreView: View {
// The StickerStore instance could be created eagerly
// and recreated during parent view updates
@State private var store = StickerStore()
var body: some View {
// ...
}
}
@Observable
class StickerStore {
init() {
print("StickerStore initialized")
// This could print multiple times before the view appeared
}
}วิธีแก้ไข: การ initialize แบบ lazy
ด้วย iOS 27 (และ backport ไปยัง iOS 17) คลาส @Observable ที่เก็บใน @State ถูก initialize แบบ lazy เพียงครั้งเดียวตลอดอายุของ view:
// iOS 27+: guaranteed single initialization
struct StickerStoreView: View {
@State private var store = StickerStore()
var body: some View {
// store is created exactly once, when the view first needs it
StoreContentView(store: store)
}
}
@Observable
class StickerStore {
var stickers: [Sticker] = []
init() {
print("StickerStore initialized")
// Now prints exactly once per view lifetime
}
}เมื่อกำหนด @State ใน initializer ให้ลบค่า default เพื่อหลีกเลี่ยง error ตอนคอมไพล์:
// Correct pattern when using custom initializer
struct StickerPageView: View {
@State private var page: StickerPage // No default value
let title: String
init(title: String) {
self.page = StickerPage(title: title)
self.title = title
}
var body: some View {
// ...
}
}การแก้ไขนี้ขจัดการจัดสรร object ที่ไม่จำเป็นและรับประกันว่า instance ของ @Observable จะคงอยู่อย่างถูกต้องตลอดวงจรชีวิตของ view สำหรับนักพัฒนาที่เตรียมตัวสำหรับคำถามสัมภาษณ์ iOS การเข้าใจการเปลี่ยนแปลงนี้แสดงถึงความรู้เกี่ยวกับ internal ของ SwiftUI ในปัจจุบัน
รูปแบบการใช้งานขั้นสูง
การรวม @State และ @Observable
ในแอปจริง เครื่องมือทั้งสองอยู่ร่วมกันได้อย่างกลมกลืน @State จัดการ state UI ในขณะที่ @Observable ห่อหุ้มข้อมูลธุรกิจ
@Observable
class TodoListModel {
var todos: [Todo] = []
var filter: TodoFilter = .all
var filteredTodos: [Todo] {
switch filter {
case .all:
return todos
case .active:
return todos.filter { !$0.isCompleted }
case .completed:
return todos.filter { $0.isCompleted }
}
}
func addTodo(title: String) {
let todo = Todo(id: UUID(), title: title, isCompleted: false)
todos.append(todo)
}
func toggleTodo(_ todo: Todo) {
guard let index = todos.firstIndex(where: { $0.id == todo.id }) else { return }
todos[index].isCompleted.toggle()
}
}
struct Todo: Identifiable, Equatable {
let id: UUID
var title: String
var isCompleted: Bool
}
enum TodoFilter: CaseIterable {
case all, active, completed
}
struct TodoListView: View {
// Business data via @Observable (lazy init since iOS 27)
@State private var model = TodoListModel()
// Local UI state via @State
@State private var newTodoTitle = ""
@State private var isAddingTodo = false
@State private var selectedTodo: Todo?
var body: some View {
NavigationStack {
VStack {
// Filter with Picker
Picker("Filter", selection: $model.filter) {
ForEach(TodoFilter.allCases, id: .self) { filter in
Text(filter.label).tag(filter)
}
}
.pickerStyle(.segmented)
.padding()
// Todo list
List(model.filteredTodos, selection: $selectedTodo) { todo in
TodoRowView(todo: todo) {
model.toggleTodo(todo)
}
}
}
.navigationTitle("Tasks")
.toolbar {
Button {
isAddingTodo = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $isAddingTodo) {
AddTodoSheet(model: model)
}
}
}
}
struct TodoRowView: View {
let todo: Todo
let onToggle: () -> Void
var body: some View {
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(todo.isCompleted ? .green : .secondary)
.onTapGesture(perform: onToggle)
Text(todo.title)
.strikethrough(todo.isCompleted)
}
}
}
extension TodoFilter {
var label: String {
switch self {
case .all: return "All"
case .active: return "Active"
case .completed: return "Completed"
}
}
}@Observable กับ dependency injection
สำหรับแอปที่ซับซ้อนขึ้น การฉีดผ่าน environment ของ SwiftUI ช่วยแยก dependency ได้อย่างมีประสิทธิภาพ
@Observable
class AuthenticationService {
var currentUser: User?
var isAuthenticated: Bool { currentUser != nil }
func login(email: String, password: String) async throws {
// Authentication logic
currentUser = User(id: UUID(), email: email, name: "User")
}
func logout() {
currentUser = nil
}
}
struct User: Identifiable, Equatable {
let id: UUID
let email: String
let name: String
}
// Extension to create an environment key
extension EnvironmentValues {
@Entry var authService: AuthenticationService = AuthenticationService()
}
// Configuration in the App
@main
struct MyApp: App {
@State private var authService = AuthenticationService()
var body: some Scene {
WindowGroup {
ContentView()
.environment(.authService, authService)
}
}
}
// Usage in views
struct ProfileView: View {
@Environment(.authService) private var authService
var body: some View {
if let user = authService.currentUser {
VStack {
Text("Hello, (user.name)")
Button("Sign Out") {
authService.logout()
}
}
} else {
Text("Not signed in")
}
}
}ประสิทธิภาพและการปรับปรุง
หลีกเลี่ยงการ re-render ที่ไม่จำเป็น
แม้ @Observable จะมีความละเอียด รูปแบบบางอย่างก็ยังลดทอนประสิทธิภาพได้
// Bad pattern: reading the entire object
struct BadPatternView: View {
let model: ProfileModel
var body: some View {
// Reads model.name AND model.posts even if only name is displayed
let _ = model.posts.count // Creates unnecessary dependency
Text(model.name)
}
}
// Good pattern: targeted reading
struct GoodPatternView: View {
let model: ProfileModel
var body: some View {
// Tracks only name
Text(model.name)
}
}
// Extract into subviews to isolate dependencies
struct OptimizedProfileView: View {
let model: ProfileModel
var body: some View {
VStack {
// Each subview has its own dependencies
ProfileNameView(model: model)
ProfilePostsView(model: model)
ProfileStatsView(model: model)
}
}
}
struct ProfileNameView: View {
let model: ProfileModel
var body: some View {
Text(model.name)
.font(.title)
}
}
struct ProfilePostsView: View {
let model: ProfileModel
var body: some View {
ForEach(model.posts) { post in
PostRow(post: post)
}
}
}
struct ProfileStatsView: View {
let model: ProfileModel
var body: some View {
HStack {
StatBadge(value: model.followerCount, label: "Followers")
StatBadge(value: model.posts.count, label: "Posts")
}
}
}Computed property บน @Observable จะถูกประเมินใหม่ทุกครั้งที่เข้าถึง สำหรับการคำนวณที่ซับซ้อน ควรแคชผลลัพธ์ไว้ใน stored property
การสังเกตขั้นสูงด้วย Swift 6.4
Swift 6.4 เปิดตัว SE-0506 เพิ่ม API แจ้งเตือนการเปลี่ยนแปลงแบบละเอียดและต่อเนื่องให้กับ Observation framework เครื่องมือขั้นสูงเหล่านี้มุ่งเป้าไปที่นักพัฒนา middleware และการ implement ระบบ widget ไม่ใช่สำหรับการพัฒนาแอปทั่วไป แอปส่วนใหญ่ยังคงใช้รูปแบบ macro @Observable มาตรฐาน บันทึกการเผยแพร่ Swift 6.4 อธิบายรายละเอียดการเพิ่มเติมเหล่านี้
การย้ายจาก ObservableObject
สำหรับโปรเจกต์ที่มีอยู่ซึ่งใช้ ObservableObject การย้ายไปใช้ @Observable ทำให้โค้ดง่ายขึ้น
// Old pattern with ObservableObject
class OldSettingsModel: ObservableObject {
@Published var darkMode: Bool = false
@Published var fontSize: CGFloat = 16
@Published var notifications: Bool = true
}
struct OldSettingsView: View {
@StateObject private var settings = OldSettingsModel()
// or @ObservedObject if injected
var body: some View {
Form {
Toggle("Dark Mode", isOn: $settings.darkMode)
Slider(value: $settings.fontSize, in: 12...24)
Toggle("Notifications", isOn: $settings.notifications)
}
}
}
// New pattern with @Observable
@Observable
class NewSettingsModel {
var darkMode: Bool = false
var fontSize: CGFloat = 16
var notifications: Bool = true
}
struct NewSettingsView: View {
@State private var settings = NewSettingsModel()
var body: some View {
Form {
Toggle("Dark Mode", isOn: $settings.darkMode)
Slider(value: $settings.fontSize, in: 12...24)
Toggle("Notifications", isOn: $settings.notifications)
}
}
}ข้อดีของการย้าย:
- ไม่ต้องใส่
@Publishedในแต่ละ property อีกต่อไป @Stateแทนที่@StateObjectในการสร้าง (รับประกันการ init แบบ lazy)- การสังเกตแบบละเอียดอัตโนมัติ
- โค้ดอ่านง่ายและดูแลง่ายขึ้น
สำหรับแอปที่รองรับ iOS 17+ แนะนำให้ย้ายไปใช้ @Observable คู่มือการย้าย Combine vs async/await ครอบคลุมรูปแบบการปรับปรุงที่เกี่ยวข้อง
กฎการตัดสินใจในทางปฏิบัติ
คู่มือการตัดสินใจในการเลือกเครื่องมือที่เหมาะสม
/*
RULE 1: Ephemeral UI state -> @State
- Animations, transitions
- Local form states
- Temporary selections
- Section expand/collapse
*/
struct AnimatedCard: View {
@State private var isFlipped = false // Local UI state
// ...
}
/*
RULE 2: Shared data across views -> @Observable
- Business data models
- Authentication state
- Shopping cart
- User preferences
*/
@Observable
class UserSession { // Shared across app
var user: User?
var preferences: Preferences
// ...
}
/*
RULE 3: Simple struct with binding -> @State
- Local configuration
- Isolated forms
*/
struct FormData {
var name: String = ""
var email: String = ""
}
struct FormView: View {
@State private var formData = FormData() // Struct with @State
// ...
}
/*
RULE 4: Complex business logic -> @Observable
- Validations
- Network calls
- Data transformations
*/
@Observable
class OrderProcessor { // Complex logic
var items: [OrderItem] = []
var status: OrderStatus = .draft
func validate() -> [ValidationError] { /* ... */ }
func submit() async throws { /* ... */ }
}แหล่งข้อมูล
- เอกสาร Observation framework - เอกสารอ้างอิง API อย่างเป็นทางการจาก Apple
- บันทึกการเผยแพร่ Swift 6.4 - การติดตามการสังเกตขั้นสูง SE-0506
- คู่มือ SwiftUI WWDC 2026 - การเปลี่ยนแปลง macro @State และการ initialize แบบ lazy
การเลือกระหว่าง @Observable และ @State ใน SwiftUI
การเลือกระหว่าง @Observable และ @State มาสู่คำถามพื้นฐานสองข้อ ได้แก่ ประเภทข้อมูล (value หรือ reference) และขอบเขตของ state (ภายในหรือใช้ร่วมกัน) @State โดดเด่นสำหรับ state UI แบบง่ายและเฉพาะในขณะที่ @Observable ส่องประกายเมื่อมีโมเดลข้อมูลซับซ้อนที่ต้องการการสังเกตแบบละเอียด
เช็กลิสต์การตัดสินใจ
- ใช้
@Stateสำหรับ value type และ state UI ชั่วคราว - ใช้
@Observableสำหรับคลาสที่มีข้อมูลธุรกิจ - เลือก
@Observableเมื่อ state ครอบคลุมหลาย view - แยกเป็น subview เพื่อปรับปรุงการ re-render
- หลีกเลี่ยงการอ่าน property ที่ไม่จำเป็นใน body
- ย้ายจาก
ObservableObjectอย่างค่อยเป็นค่อยไป - ใช้ environment สำหรับ dependency injection
- ทดสอบประสิทธิภาพด้วย Instruments สำหรับเคสซับซ้อน
ด้วยการปรับปรุง macro @State ใน iOS 27 (backport ไปยัง iOS 17) การรวมเครื่องมือทั้งสองเข้าด้วยกันเชื่อถือได้มากกว่าที่เคย การแก้ไขการ initialize แบบ lazy ขจัดแหล่งที่มาของบั๊กทั่วไปเมื่อเก็บคลาส @Observable ใน @State
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
คุณหาบั๊กใน iOS เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 5 ตุลาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

งาน iOS Developer ปี 2026: แหล่งหางาน ช่วงเงินเดือน และการเตรียมตัวสัมภาษณ์
คู่มือครบถ้วนสำหรับการหางาน iOS Developer ในปี 2026 ค้นพบแหล่งหางานที่ดีที่สุด ข้อมูลเงินเดือนล่าสุด และกลยุทธ์การเตรียมตัวสัมภาษณ์เทคนิค

ประสิทธิภาพ SwiftUI: การปรับแต่ง LazyVStack และรายการที่ซับซ้อน
เทคนิคการปรับแต่งสำหรับ LazyVStack และรายการ SwiftUI ลดการใช้หน่วยความจำ ปรับปรุงประสิทธิภาพการเลื่อน และหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย

ViewModifier แบบกำหนดเองใน SwiftUI: รูปแบบที่นำกลับมาใช้ใหม่ได้สำหรับ Design System
สร้าง ViewModifier แบบกำหนดเองใน SwiftUI สำหรับ design system ที่สอดคล้องกัน รูปแบบ แนวทางปฏิบัติที่ดีที่สุด และตัวอย่างที่ใช้งานได้จริงสำหรับการจัดสไตล์ view ของ iOS อย่างมีประสิทธิภาพ