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.

So sánh @Observable và @State trong SwiftUI dành cho lập trình viên iOS

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. Bao gồm các thay đổi macro @State ở iOS 27 (backport về iOS 17).

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ó.

CounterView.swiftswift
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:

StateCharacteristics.swiftswift
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
}

Đ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. Tài liệu framework Observation cung cấp tham chiếu API đầy đủ.

UserModel.swiftswift
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:

GranularObservation.swiftswift
@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:

ComparisonExample.swiftswift
// 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ệuKiểu giá trị (struct, enum)Class
Phạm viCục bộ trong một viewCó thể chia sẻ giữa các view
Độ phức tạpState đơn giảnLogic nghiệp vụ phức tạp
Vòng đờiĐược SwiftUI quản lýQuản lý rõ ràng
Render lạiToàn bộ viewChi tiết theo từng thuộc tính

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Cập nhật iOS 27: @State dưới dạng macro với khởi tạo lazy

WWDC 2026 giới thiệu thay đổi quan trọng cho @State: giờ được triển khai như một macro thay vì property wrapper. Thay đổi này trực tiếp giải quyết vấn đề phổ biến khi sử dụng @State với các class @Observable.

Vấn đề trước iOS 27

Trước đây, các class @Observable được lưu trong @State được khởi tạo một cách eager. Điều này có thể khiến cùng một instance được tạo nhiều lần trước khi view xuất hiện:

BeforeIOS27.swiftswift
// 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
    }
}

Giải pháp: khởi tạo lazy

Với iOS 27 (và được backport về iOS 17), các class @Observable được lưu trong @State giờ được khởi tạo lazy, đúng một lần cho suốt vòng đời của view:

AfterIOS27.swiftswift
// 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
    }
}
Thay đổi mẫu initializer

Khi gán @State trong initializer, bỏ giá trị mặc định để tránh lỗi biên dịch:

InitializerPattern.swiftswift
// 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 {
        // ...
    }
}

Bản sửa lỗi này loại bỏ việc cấp phát đối tượng lãng phí và đảm bảo các instance @Observable tồn tại đúng cách trong suốt vòng đời view. Đối với các nhà phát triển chuẩn bị cho câu hỏi phỏng vấn iOS, hiểu thay đổi này thể hiện kiến thức về nội bộ SwiftUI hiện tại.

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ụ.

CombinedPatterns.swiftswift
@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 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ả:

DependencyInjection.swiftswift
@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:

PerformanceOptimization.swiftswift
// 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.

Quan sát nâng cao với Swift 6.4

Swift 6.4 giới thiệu SE-0506, thêm các API thông báo thay đổi chi tiết và liên tục vào framework Observation. Các công cụ nâng cao này nhắm đến các nhà phát triển middleware và triển khai hệ thống widget, không phải cho phát triển ứng dụng thông thường. Hầu hết các ứng dụng vẫn sử dụng mẫu macro @Observable tiêu chuẩn. Ghi chú phát hành Swift 6.4 chi tiết các bổ sung này.

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ã:

MigrationExample.swiftswift
// 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 (với đảm bảo init lazy)
  • Quan sát chi tiết tự động
  • Mã dễ đọc và dễ bảo trì hơn

Đối với các ứng dụng nhắm đến iOS 17+, nên chuyển sang @Observable. Hướng dẫn chuyển đổi Combine vs async/await đề cập các mẫu hiện đại hóa liên quan.

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:

DecisionGuide.swiftswift
/*
 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 { /* ... */ }
}

Nguồn tham khảo

Lựa chọn giữa @Observable và @State trong SwiftUI

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

Với các cải tiến macro @State ở iOS 27 (backport về iOS 17), việc kết hợp hai công cụ này đáng tin cậy hơn bao giờ hết. Bản sửa khởi tạo lazy loại bỏ nguồn bug phổ biến khi lưu class @Observable trong @State.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thử thách hôm nay

Bạn có tìm ra lỗi trong iOS không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 5 tháng 10, 2026

Thẻ

#swiftui
#ios
#observable
#state-management
#swift

Chia sẻ

Bài viết liên quan