import Combine
import Foundation
class SearchViewModel: ObservableObject {
@Published var searchQuery = ""
@Published var results: [SearchResult] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let searchService: SearchService
private var cancellables = Set<AnyCancellable>()
init(searchService: SearchService = .shared) {
self.searchService = searchService
setupSearchPipeline()
}
private func setupSearchPipeline() {
// Complex pipeline with multiple operators
$searchQuery
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { $0.count >= 3 }
.handleEvents(receiveOutput: { [weak self] _ in
self?.isLoading = true
self?.errorMessage = nil
})
.flatMap { [weak self] query -> AnyPublisher<[SearchResult], Never> in
guard let self = self else {
return Just([]).eraseToAnyPublisher()
}
return self.searchService.search(query: query)
.retry(2)
.catch { error -> AnyPublisher<[SearchResult], Never> in
self.errorMessage = error.localizedDescription
return Just([]).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.sink { [weak self] results in
self?.results = results
self?.isLoading = false
}
.store(in: &cancellables)
}
}
class FormViewModel: ObservableObject {
@Published var username = ""
@Published var email = ""
@Published var password = ""
@Published var confirmPassword = ""
@Published var isFormValid = false
@Published var validationErrors: [String: String] = [:]
private var cancellables = Set<AnyCancellable>()
init() {
setupValidation()
}
private func setupValidation() {
// Username validation
let usernameValid = $username
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.map { $0.count >= 3 }
.share()
usernameValid
.map { valid in
valid ? nil : "Username must be at least 3 characters"
}
.sink { [weak self] error in
self?.validationErrors["username"] = error
}
.store(in: &cancellables)
// Email validation
let emailValid = $email
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.map { email in
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email)
}
.share()
emailValid
.map { valid in
valid ? nil : "Invalid email address"
}
.sink { [weak self] error in
self?.validationErrors["email"] = error
}
.store(in: &cancellables)
// Password validation
let passwordValid = $password
.map { $0.count >= 8 }
.share()
// Passwords match validation
let passwordsMatch = Publishers.CombineLatest($password, $confirmPassword)
.map { $0 == $1 && !$0.isEmpty }
.share()
passwordsMatch
.map { match in
match ? nil : "Passwords do not match"
}
.sink { [weak self] error in
self?.validationErrors["confirmPassword"] = error
}
.store(in: &cancellables)
// Overall form validation
Publishers.CombineLatest4(
usernameValid,
emailValid,
passwordValid,
passwordsMatch
)
.map { $0 && $1 && $2 && $3 }
.assign(to: &$isFormValid)
}
}
// Example of advanced operators
class DataSyncViewModel: ObservableObject {
@Published var localData: [Post] = []
@Published var remoteData: [Post] = []
@Published var syncedData: [Post] = []
private var cancellables = Set<AnyCancellable>()
init() {
// Merge and deduplicate data from multiple sources
Publishers.Merge($localData, $remoteData)
.collect()
.map { allPosts in
var uniquePosts: [Int: Post] = [:]
for post in allPosts.flatMap({ $0 }) {
uniquePosts[post.id] = post
}
return Array(uniquePosts.values).sorted { $0.createdAt > $1.createdAt }
}
.assign(to: &$syncedData)
// Time-based operations
Timer.publish(every: 60, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.refreshData()
}
.store(in: &cancellables)
}
private func refreshData() {
// Refresh logic
}
}
Combine operators transform, filter, and combine publisher streams. I chain operators to build complex async pipelines declaratively. map transforms values, filter removes unwanted elements, removeDuplicates prevents redundant updates. debounce delays emissions for search inputs, throttle limits update frequency. flatMap handles dependent async operations, zip combines multiple publishers. catch provides fallback values on errors, retry retries failed operations. combineLatest merges publishers, emitting when any changes. The sink subscriber receives final values. Understanding operator timing and memory management prevents common bugs. Combine eliminates callback pyramids, making async code readable and testable.