# SwiftUI @Observable vs @State: Khi Nào Dùng Cái Nào Năm 2026 > Nắm vững sự khác biệt giữa @Observable và @State trong SwiftUI để chọn công cụ quản lý state phù hợp cho ứng dụng iOS. - Published: 2026-02-25 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, observable, state-management, swift - Reading time: 14 min --- Quản lý state là nền tảng của mọi ứng dụng SwiftUI có hiệu năng cao. Kể từ iOS 17, macro `@Observable` đã cách mạng hóa cách tạo các model phản ứng, trong khi `@State` vẫn không thể thiếu cho state cục bộ của view. Hiểu rõ khi nào sử dụng từng công cụ giúp tránh các lần render lại không cần thiết và xây dựng những ứng dụng mượt mà, đáp ứng tốt. > **Bài viết này đề cập đến** > > Bài viết khám phá cơ chế nội bộ của `@Observable` và `@State`, sự khác biệt cơ bản của chúng, đồng thời cung cấp hướng dẫn rõ ràng để chọn công cụ phù hợp theo ngữ cảnh. ## Cơ bản về @State `@State` là dạng quản lý state đơn giản nhất trong SwiftUI. Property wrapper này tạo bộ nhớ bền vững cho một giá trị thuộc riêng view khai báo nó. ```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) } } } ``` Mỗi lần thay đổi `count` sẽ kích hoạt việc render lại view. SwiftUI tự động quản lý vòng đời của giá trị này, giữ nguyên qua các lần xây dựng lại body. ### Đặc điểm chính của @State `@State` sở hữu một số đặc tính nổi bật xác định việc sử dụng tối ưu: ```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 } ``` Điểm quan trọng: `@State` hoạt động với các kiểu giá trị (struct, enum, kiểu nguyên thủy). Đối với kiểu tham chiếu (class), cần các công cụ khác. ## Macro @Observable được giải thích Được giới thiệu cùng iOS 17, `@Observable` biến bất kỳ class nào thành một nguồn dữ liệu phản ứng. Khác với protocol `ObservableObject` cũ, macro này cung cấp quan sát chi tiết: chỉ những thuộc tính thực sự được view đọc mới kích hoạt việc render lại. ```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 } ``` Điều kỳ diệu xảy ra tại thời điểm biên dịch: macro tự động tạo mã theo dõi cần thiết cho từng thuộc tính. ### Quan sát chi tiết trong thực tế Khác biệt lớn so với `ObservableObject` cũ nằm ở mức độ chi tiết của việc theo dõi: ```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 } } } } ``` > **Theo dõi tự động** > > SwiftUI phân tích `body` của mỗi view để xác định những thuộc tính nào được đọc. Chỉ những thuộc tính đó mới kích hoạt việc render lại khi bị thay đổi. ## So sánh trực tiếp: @Observable vs @State Việc lựa chọn giữa hai công cụ này phụ thuộc vào nhiều yếu tố. Đây là bảng so sánh có cấu trúc: ```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 } ``` ### Bảng tổng hợp các trường hợp sử dụng | Tiêu chí | @State | @Observable | |----------|--------|-------------| | Loại dữ liệu | Kiểu giá trị (struct, enum) | Class | | Phạm vi | Cục bộ trong một view | Có thể chia sẻ giữa các view | | Độ phức tạp | State đơn giản | Logic nghiệp vụ phức tạp | | Vòng đời | Được SwiftUI quản lý | Quản lý rõ ràng | | Render lại | Toàn bộ view | Chi tiết theo từng thuộc tính | ## Mẫu sử dụng nâng cao ### Kết hợp @State và @Observable Trong các ứng dụng thực tế, hai công cụ này tồn tại hài hòa cùng nhau. `@State` xử lý state UI cục bộ trong khi `@Observable` đóng gói dữ liệu nghiệp vụ. ```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 với dependency injection Đối với các ứng dụng phức tạp hơn, việc tiêm phụ thuộc qua environment của SwiftUI cho phép tách biệt hiệu quả: ```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") } } } ``` ## Hiệu năng và tối ưu hóa ### Tránh render lại không cần thiết Ngay cả với mức chi tiết của `@Observable`, một số mẫu vẫn có thể làm giảm hiệu năng: ```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 tốn kém** > > Các computed property trên `@Observable` được đánh giá lại mỗi lần truy cập. Đối với các tính toán phức tạp, nên cache kết quả vào stored property. ### Cập nhật theo lô với withObservationTracking Với các kịch bản nâng cao, `withObservationTracking` cho phép phát hiện thay đổi mà không cần tạo 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() } } ``` ## Chuyển từ ObservableObject Đối với các dự án hiện tại đang dùng `ObservableObject`, việc chuyển sang `@Observable` đơn giản hóa mã: ```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) } } } ``` Lợi ích của việc chuyển đổi: - Không cần `@Published` trên từng thuộc tính nữa - `@State` thay thế `@StateObject` khi tạo - Quan sát chi tiết tự động - Mã dễ đọc và dễ bảo trì hơn ## Quy tắc thực tế khi ra quyết định Hướng dẫn ra quyết định để chọn công cụ phù hợp: ```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 { /* ... */ } } ``` ## Kết luận Việc lựa chọn giữa `@Observable` và `@State` quy về hai câu hỏi cơ bản: loại dữ liệu (giá trị hay tham chiếu) và phạm vi state (cục bộ hay chia sẻ). `@State` xuất sắc cho state UI đơn giản và cục bộ, trong khi `@Observable` tỏa sáng với các model dữ liệu phức tạp đòi hỏi quan sát chi tiết. ### Danh sách kiểm tra ra quyết định - ✅ Dùng `@State` cho kiểu giá trị và state UI tạm thời - ✅ Dùng `@Observable` cho class chứa dữ liệu nghiệp vụ - ✅ Ưu tiên `@Observable` khi state trải dài qua nhiều view - ✅ Tách thành subview để tối ưu render lại - ✅ Tránh đọc các thuộc tính không cần thiết trong body - ✅ Chuyển dần từ `ObservableObject` - ✅ Sử dụng environment cho dependency injection - ✅ Kiểm tra hiệu năng với Instruments cho các trường hợp phức tạp --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/swiftui-observable-vs-state