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
| 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ế
Last updated on