# SwiftUI @Observable vs @State: ใช้ตัวไหนเมื่อไหร่ในปี 2026 > เข้าใจความแตกต่างระหว่าง @Observable และ @State ใน SwiftUI เพื่อเลือกเครื่องมือจัดการ state ที่เหมาะกับแอป iOS - Published: 2026-02-25 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, observable, state-management, swift - Reading time: 14 min --- การจัดการ state คือรากฐานของแอป SwiftUI ที่มีประสิทธิภาพ ตั้งแต่ iOS 17 เป็นต้นมา macro `@Observable` ได้พลิกโฉมการสร้างโมเดลแบบ reactive ในขณะที่ `@State` ยังคงสำคัญสำหรับ state ภายในของ view การเข้าใจว่าควรใช้เครื่องมือใดเมื่อไรช่วยลดการ re-render ที่ไม่จำเป็นและทำให้สร้างแอปที่ลื่นไหลและตอบสนองได้ดี > **สิ่งที่บทความนี้ครอบคลุม** > > บทความนี้สำรวจกลไกภายในของ `@Observable` และ `@State` ความแตกต่างพื้นฐาน และแนวทางที่ชัดเจนในการเลือกเครื่องมือที่เหมาะสมตามบริบท ## พื้นฐาน @State `@State` คือรูปแบบการจัดการ state ที่ง่ายที่สุดใน SwiftUI property wrapper นี้สร้างที่จัดเก็บข้อมูลถาวรสำหรับค่าที่เป็นของ view ที่ประกาศมันโดยเฉพาะ ```swift // CounterView.swift 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` มีคุณสมบัติเด่นหลายประการที่กำหนดการใช้งานที่เหมาะสม ```swift // StateCharacteristics.swift struct FormView: View { // ✅ Simple local state - 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 ```swift // UserModel.swift 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` แบบเก่าอยู่ที่ความละเอียดของการติดตาม ```swift // GranularObservation.swift @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 การเลือกระหว่างสองเครื่องมือนี้ขึ้นอยู่กับหลายปัจจัย ต่อไปนี้คือการเปรียบเทียบที่มีโครงสร้าง ```swift // ComparisonExample.swift // 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 | ## รูปแบบการใช้งานขั้นสูง ### การรวม @State และ @Observable ในแอปจริง เครื่องมือทั้งสองอยู่ร่วมกันได้อย่างกลมกลืน `@State` จัดการ state UI ในขณะที่ `@Observable` ห่อหุ้มข้อมูลธุรกิจ ```swift // CombinedPatterns.swift @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 @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 ได้อย่างมีประสิทธิภาพ ```swift // DependencyInjection.swift @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` จะมีความละเอียด รูปแบบบางอย่างก็ยังลดทอนประสิทธิภาพได้ ```swift // PerformanceOptimization.swift // ❌ 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 ที่มีต้นทุนสูง** > > Computed property บน `@Observable` จะถูกประเมินใหม่ทุกครั้งที่เข้าถึง สำหรับการคำนวณที่ซับซ้อน ควรแคชผลลัพธ์ไว้ใน stored property ### อัปเดตเป็นชุดด้วย withObservationTracking สำหรับสถานการณ์ขั้นสูง `withObservationTracking` ช่วยตรวจจับการเปลี่ยนแปลงโดยไม่ต้องสร้าง binding ```swift // ObservationTracking.swift import Observation @Observable class DataSyncModel { var lastSyncDate: Date? var pendingChanges: Int = 0 var isSyncing: Bool = false } class SyncCoordinator { let model: DataSyncModel init(model: DataSyncModel) { self.model = model startObserving() } private func startObserving() { // Observe changes without UI withObservationTracking { // Access that creates dependencies _ = model.pendingChanges _ = model.isSyncing } onChange: { // Called when an observed property changes Task { @MainActor in self.handleModelChange() } } } private func handleModelChange() { if model.pendingChanges > 0 && !model.isSyncing { // Trigger synchronization Task { await syncChanges() } } // Re-establish observation startObserving() } private func syncChanges() async { model.isSyncing = true // Sync logic... model.isSyncing = false model.pendingChanges = 0 model.lastSyncDate = Date() } } ``` ## การย้ายจาก ObservableObject สำหรับโปรเจกต์ที่มีอยู่ซึ่งใช้ `ObservableObject` การย้ายไปใช้ `@Observable` ทำให้โค้ดง่ายขึ้น ```swift // MigrationExample.swift // ❌ 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` ในการสร้าง - การสังเกตแบบละเอียดอัตโนมัติ - โค้ดอ่านง่ายและดูแลง่ายขึ้น ## กฎการตัดสินใจในทางปฏิบัติ คู่มือการตัดสินใจในการเลือกเครื่องมือที่เหมาะสม ```swift // DecisionGuide.swift /* 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 { /* ... */ } } ``` ## สรุป การเลือกระหว่าง `@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 สำหรับเคสซับซ้อน --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/swiftui-observable-vs-state