Skip to Content
iOS📊 State ManagementObservableObject

ObservableObject trong SwiftUI

1. Giới thiệu

ObservableObject cho phép class publish thay đổi để Views cập nhật.

class CounterViewModel: ObservableObject { @Published var count = 0 func increment() { count += 1 } } struct ContentView: View { @StateObject var viewModel = CounterViewModel() var body: some View { VStack { Text("Count: \(viewModel.count)") Button("Increment") { viewModel.increment() } } } }

2. @Published

Đánh dấu properties cần notify khi thay đổi.

class UserSettings: ObservableObject { @Published var username = "" @Published var isLoggedIn = false @Published var theme = "light" var nonPublishedValue = 0 // Không trigger update }

3. @StateObject vs @ObservedObject

@StateObject - Own object

struct ParentView: View { @StateObject var viewModel = MyViewModel() // Tạo và own var body: some View { ChildView(viewModel: viewModel) } }

@ObservedObject - Reference object

struct ChildView: View { @ObservedObject var viewModel: MyViewModel // Nhận từ parent var body: some View { Text(viewModel.text) } }

Quy tắc: Dùng @StateObject khi tạo, @ObservedObject khi nhận.

4. ViewModel Pattern

class ProductListViewModel: ObservableObject { @Published var products: [Product] = [] @Published var isLoading = false @Published var errorMessage: String? func loadProducts() { isLoading = true // Simulate API call DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.products = [ Product(name: "iPhone", price: 999), Product(name: "MacBook", price: 1999) ] self.isLoading = false } } } struct ProductListView: View { @StateObject var viewModel = ProductListViewModel() var body: some View { Group { if viewModel.isLoading { ProgressView() } else { List(viewModel.products) { product in HStack { Text(product.name) Spacer() Text("$\(product.price)") } } } } .onAppear { viewModel.loadProducts() } } }

5. Async/Await trong ViewModel

@MainActor class NewsViewModel: ObservableObject { @Published var articles: [Article] = [] @Published var isLoading = false func fetchNews() async { isLoading = true do { let url = URL(string: "https://api.example.com/news")! let (data, _) = try await URLSession.shared.data(from: url) articles = try JSONDecoder().decode([Article].self, from: data) } catch { print("Error: \(error)") } isLoading = false } } struct NewsView: View { @StateObject var viewModel = NewsViewModel() var body: some View { List(viewModel.articles) { article in Text(article.title) } .task { await viewModel.fetchNews() } } }

6. Form ViewModel

class LoginViewModel: ObservableObject { @Published var email = "" @Published var password = "" @Published var isLoading = false @Published var error: String? @Published var isAuthenticated = false var isFormValid: Bool { email.contains("@") && password.count >= 6 } @MainActor func login() async { guard isFormValid else { return } isLoading = true error = nil // Simulate login try? await Task.sleep(nanoseconds: 1_000_000_000) if email == "[email protected]" && password == "123456" { isAuthenticated = true } else { error = "Invalid credentials" } isLoading = false } } struct LoginView: View { @StateObject var viewModel = LoginViewModel() var body: some View { VStack(spacing: 16) { TextField("Email", text: $viewModel.email) .textFieldStyle(.roundedBorder) .autocapitalization(.none) SecureField("Password", text: $viewModel.password) .textFieldStyle(.roundedBorder) if let error = viewModel.error { Text(error) .foregroundColor(.red) .font(.caption) } Button { Task { await viewModel.login() } } label: { if viewModel.isLoading { ProgressView() } else { Text("Login") .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) .disabled(!viewModel.isFormValid || viewModel.isLoading) } .padding() } }

7. Combine với ObservableObject

import Combine class SearchViewModel: ObservableObject { @Published var searchText = "" @Published var results: [String] = [] private var cancellables = Set<AnyCancellable>() init() { // Debounce search $searchText .debounce(for: .milliseconds(500), scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] text in self?.search(query: text) } .store(in: &cancellables) } private func search(query: String) { guard !query.isEmpty else { results = [] return } // Perform search results = ["Result 1", "Result 2", "Result 3"] .filter { $0.localizedCaseInsensitiveContains(query) } } }

8. @Observable (iOS 17+)

Macro mới thay thế ObservableObject.

@Observable class Counter { var count = 0 func increment() { count += 1 } } struct ContentView: View { @State var counter = Counter() var body: some View { VStack { Text("Count: \(counter.count)") Button("Increment") { counter.increment() } } } }

📝 Tóm tắt

WrapperKhi dùng
@StateObjectTạo và own ObservableObject
@ObservedObjectNhận reference từ parent
@PublishedProperty cần notify

iOS 17+: Dùng @Observable macro thay thế

Last updated on