Combine operators for data transformation

Sofia Martinez Jan 2026
1 tab
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
    }
}
1 file · swift Explain with highlit

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.