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

การจัดการ state คือรากฐานของแอป SwiftUI ที่มีประสิทธิภาพ ตั้งแต่ iOS 17 เป็นต้นมา macro @Observable ได้พลิกโฉมการสร้างโมเดลแบบ reactive ในขณะที่ @State ยังคงสำคัญสำหรับ state ภายในของ view การเข้าใจว่าควรใช้เครื่องมือใดเมื่อไรช่วยลดการ re-render ที่ไม่จำเป็นและทำให้สร้างแอปที่ลื่นไหลและตอบสนองได้ดี
บทความนี้สำรวจกลไกภายในของ @Observable และ @State ความแตกต่างพื้นฐาน และแนวทางที่ชัดเจนในการเลือกเครื่องมือที่เหมาะสมตามบริบท
พื้นฐาน @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 - 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
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 และแบบทดสอบเทคนิคครับ
รูปแบบการใช้งานขั้นสูง
การรวม @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
@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
อัปเดตเป็นชุดด้วย withObservationTracking
สำหรับสถานการณ์ขั้นสูง withObservationTracking ช่วยตรวจจับการเปลี่ยนแปลงโดยไม่ต้องสร้าง binding
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 ทำให้โค้ดง่ายขึ้น
// ❌ 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ในการสร้าง- การสังเกตแบบละเอียดอัตโนมัติ
- โค้ดอ่านง่ายและดูแลง่ายขึ้น
กฎการตัดสินใจในทางปฏิบัติ
คู่มือการตัดสินใจในการเลือกเครื่องมือที่เหมาะสม
/*
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 สำหรับเคสซับซ้อน
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

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

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

SwiftUI: การสร้างอินเทอร์เฟซที่ทันสมัยสำหรับ iOS
คู่มือการสร้างอินเทอร์เฟซที่ทันสมัยด้วย SwiftUI: ไวยากรณ์แบบ declarative, คอมโพเนนต์, แอนิเมชัน และแนวปฏิบัติที่ดีที่สุดสำหรับ iOS 18