ObservableObject
1. Giới thiệu
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “3. @StateObject vs @ObservedObject”@StateObject - Own object
Phần tiêu đề “@StateObject - Own object”struct ParentView: View { @StateObject var viewModel = MyViewModel() // Tạo và own
var body: some View { ChildView(viewModel: viewModel) }}@ObservedObject - Reference object
Phần tiêu đề “@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
Phần tiêu đề “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
Phần tiêu đề “5. Async/Await trong ViewModel”@MainActorclass 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
Phần tiêu đề “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 == "test@example.com" && 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
Phần tiêu đề “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+)
Phần tiêu đề “8. @Observable (iOS 17+)”Macro mới thay thế ObservableObject.
@Observableclass 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
Phần tiêu đề “📝 Tóm tắt”| Wrapper | Khi dùng |
|---|---|
@StateObject |
Tạo và own ObservableObject |
@ObservedObject |
Nhận reference từ parent |
@Published |
Property cần notify |
iOS 17+: Dùng @Observable macro thay thế