Practical examples of examples of using Combine framework in Swift

If you’re learning Combine, staring at the operators list in Xcode is a fast way to get overwhelmed. What actually helps is seeing real, practical examples of using Combine framework in Swift: networking, form validation, UI bindings, and error handling that you’d ship in a production app. This guide focuses on those real examples, not toy snippets. We’ll walk through several example of Combine in action: chaining URLSession calls, debouncing search input, validating login forms, reacting to notifications, integrating with async/await, and more. The goal is simple: after reading, you should be able to look at your existing UIKit or SwiftUI code and say, “I know exactly where Combine would clean this up.” Along the way, I’ll call out 2024-era realities: Combine coexisting with async/await, where it still shines, and patterns that age well. If you want the best examples that actually match how modern iOS apps are written, you’re in the right place.
Written by
Jamie
Published

Real-world examples of using Combine framework in Swift

Let’s skip the theory and start with concrete code. These are real examples of using Combine framework in Swift that map directly to day‑to‑day app work: networking, search, validation, and state management.

Network request pipeline: a classic example of Combine in production

Networking is still the most common example of using Combine in Swift. Here’s a simple API client that fetches and decodes JSON into a model using URLSession.DataTaskPublisher.

import Combine
import Foundation

struct User: Decodable {
    let id: Int
    let name: String
    let email: String
}

enum APIError: Error {
    case invalidResponse
}

final class APIClient {
    private let baseURL = URL(string: "https://jsonplaceholder.typicode.com")!

    func fetchUser(id: Int) -> AnyPublisher<User, Error> {
        let url = baseURL.appendingPathComponent("users/\(id)")

        return URLSession.shared.dataTaskPublisher(for: url)
            .tryMap { output -> Data in
                guard let response = output.response as? HTTPURLResponse,
                      200..<300 ~= response.statusCode else {
                    throw APIError.invalidResponse
                }
                return output.data
            }
            .decode(type: User.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

Why this matters in 2024: Combine still gives you a clean, declarative pipeline for networking, especially when you already use publishers elsewhere in your app. This is one of the best examples of Combine replacing callback pyramids with a readable flow.

Chaining dependent network calls with Combine

A more realistic example of using Combine framework in Swift is when one API call depends on the result of another. Think: fetch a user, then fetch that user’s posts.

struct Post: Decodable {
    let id: Int
    let userId: Int
    let title: String
}

final class ChainedAPIClient {
    private let baseURL = URL(string: "https://jsonplaceholder.typicode.com")!

    func fetchUserAndPosts(id: Int) -> AnyPublisher<(User, [Post]), Error> {
        let userURL = baseURL.appendingPathComponent("users/\(id)")

        return URLSession.shared.dataTaskPublisher(for: userURL)
            .map(\.$data)
            .decode(type: User.self, decoder: JSONDecoder())
            .flatMap { user -> AnyPublisher<(User, [Post]), Error> in
                let postsURL = self.baseURL.appendingPathComponent("posts?userId=\(user.id)")

                return URLSession.shared.dataTaskPublisher(for: postsURL)
                    .map(\.$data)
                    .decode(type: [Post].self, decoder: JSONDecoder())
                    .map { posts in (user, posts) }
                    .eraseToAnyPublisher()
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

This is a strong example of how Combine pipelines read like a story: get user → use user.id → get posts → combine results. For many teams, examples include exactly this kind of dependent request logic.

UI-focused examples of using Combine framework in Swift

The next set of examples of using Combine framework in Swift focuses on UI: text fields, validation, and search. These are the patterns that make UIKit code feel much less brittle.

Debounced search bar in UIKit

Live search is a textbook example of using Combine in Swift. You don’t want to hit the network on every keystroke, so you debounce input and cancel in‑flight requests when the query changes.

import UIKit
import Combine

final class SearchViewController: UIViewController {
    @IBOutlet private weak var searchTextField: UITextField!
    @IBOutlet private weak var resultsLabel: UILabel!

    private var cancellables = Set<AnyCancellable>()
    private let apiClient = APIClient()

    override func viewDidLoad() {
        super.viewDidLoad()
        bindSearch()
    }

    private func bindSearch() {
        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification,
                                             object: searchTextField)
            .compactMap { ($0.object as? UITextField)?.text }
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { [apiClient] query in
                apiClient.searchUsers(matching: query) // Your own API method
                    .catch { _ in Just([]) } // Graceful error handling
            }
            .sink { [weak self] users in
                self?.resultsLabel.text = "Found: \(users.count) users"
            }
            .store(in: &cancellables)
    }
}

This pattern—notifications → debounce → filter → network call—is one of the best examples to show how Combine turns a messy set of delegates and timers into a single, readable pipeline.

Simple form validation with Combine publishers

Form validation is another real example of using Combine framework in Swift that ships in almost every app: login, registration, payment screens, you name it.

import Combine

final class LoginViewModel: ObservableObject {
    // Inputs
    @Published var email: String = ""
    @Published var password: String = ""

    // Outputs
    @Published private(set) var isValid: Bool = false
    @Published private(set) var errorMessage: String? = nil

    private var cancellables = Set<AnyCancellable>()

    init() {
        Publishers.CombineLatest(\(email, \)password)
            .map { email, password -> (Bool, String?) in
                guard email.contains("@") else {
                    return (false, "Please enter a valid email address.")
                }
                guard password.count >= 8 else {
                    return (false, "Password must be at least 8 characters.")
                }
                return (true, nil)
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isValid, message in
                self?.isValid = isValid
                self?.errorMessage = message
            }
            .store(in: &cancellables)
    }
}

Hook this up to SwiftUI or UIKit and you get real‑time validation without scattering logic across view controllers. Among all examples of using Combine framework in Swift, this one gives you a fast win and clean separation of concerns.

State management and SwiftUI: examples include @Published and ObservableObject

Combine underpins SwiftUI’s data flow model, and that alone makes it worth understanding in 2024.

SwiftUI view model powered by Combine

Here’s an example of an ObservableObject using Combine to expose state to SwiftUI, while still using publishers internally for side effects.

import SwiftUI
import Combine

final class UsersViewModel: ObservableObject {
    @Published private(set) var users: [User] = []
    @Published private(set) var isLoading: Bool = false
    @Published private(set) var error: String? = nil

    private let apiClient = APIClient()
    private var cancellables = Set<AnyCancellable>()

    func loadUsers() {
        isLoading = true
        error = nil

        apiClient.fetchAllUsers() // AnyPublisher<[User], Error>
            .receive(on: DispatchQueue.main)
            .sink { [weak self] completion in
                self?.isLoading = false
                if case let .failure(err) = completion {
                    self?.error = err.localizedDescription
                }
            } receiveValue: { [weak self] users in
                self?.users = users
            }
            .store(in: &cancellables)
    }
}

struct UsersView: View {
    @StateObject private var viewModel = UsersViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView()
            } else if let error = viewModel.error {
                Text("Error: \(error)")
            } else {
                List(viewModel.users, id: \.id) { user in
                    Text(user.name)
                }
            }
        }
        .onAppear { viewModel.loadUsers() }
    }
}

This is a clean example of using Combine framework in Swift where SwiftUI is the consumer, but Combine still orchestrates the asynchronous work.

System integration: NotificationCenter and timers

Sometimes the best examples are the small, boring ones that you reuse everywhere.

Observing NotificationCenter with Combine

NotificationCenter is a classic example of legacy APIs that become nicer with Combine.

final class KeyboardObserver {
    @Published private(set) var keyboardHeight: CGFloat = 0

    private var cancellables = Set<AnyCancellable>()

    init() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
            .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
            .map { $0.height }
            .removeDuplicates()
            .assign(to: &$keyboardHeight)
    }
}

This tiny example of Combine makes layout code far less scattered. Views can simply observe keyboardHeight instead of each controller wiring its own observer.

Timer publisher for auto-refresh

Periodic refresh is another straightforward example of using Combine framework in Swift.

final class AutoRefreshViewModel: ObservableObject {
    @Published private(set) var lastUpdated: Date = Date()

    private var cancellables = Set<AnyCancellable>()

    init() {
        Timer.publish(every: 60, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] date in
                self?.lastUpdated = date
                // Trigger your refresh logic here
            }
            .store(in: &cancellables)
    }
}

For dashboards, stock tickers, or health‑style apps that show live metrics, examples include exactly this timer pattern.

Combine and async/await: modern 2024 examples

By 2024, most new Swift code uses async/await for simple asynchronous work. That doesn’t make Combine obsolete; it just changes how you use it. The best examples now show Combine and structured concurrency working together.

Bridging a Combine publisher into async/await

Sometimes you have a publisher‑based API but you’re writing new async code. You can bridge easily:

extension Publisher {
    func firstValue() async throws -> Output {
        try await withCheckedThrowingContinuation { continuation in
            var cancellable: AnyCancellable?

            cancellable = self
                .first()
                .sink { completion in
                    if case let .failure(error) = completion {
                        continuation.resume(throwing: error)
                    }
                    _ = cancellable // keep alive
                } receiveValue: { value in
                    continuation.resume(returning: value)
                }
        }
    }
}

// Usage
// let user: User = try await apiClient.fetchUser(id: 1).firstValue()

This is a very practical example of using Combine framework in Swift in 2024: you keep your existing pipelines, but you expose async entry points for newer code.

When Combine still shines in 2024–2025

Patterns where Combine remains a strong fit:

  • Continuous streams of values (search text, sensors, WebSockets)
  • Complex operator chains (retry/backoff, debounced validation)
  • Integrating multiple event sources (notifications, timers, network)

If you’re looking for real examples of using Combine framework in Swift that still make sense today, focus on streams, not one‑off calls.

Testing examples of Combine pipelines

No discussion of examples of using Combine framework in Swift is complete without touching on tests. One of the underrated strengths of Combine is how testable your logic becomes when it’s expressed as pure publishers.

Testing a validation pipeline

Here’s a minimal example of testing the login validation logic from earlier.

import XCTest
import Combine

final class LoginViewModelTests: XCTestCase {
    private var cancellables = Set<AnyCancellable>()

    func testValidCredentialsProduceIsValidTrue() {
        let viewModel = LoginViewModel()
        let expectation = expectation(description: "isValid becomes true")

        viewModel.$isValid
            .dropFirst() // ignore initial value
            .sink { isValid in
                if isValid {
                    expectation.fulfill()
                }
            }
            .store(in: &cancellables)

        viewModel.email = "user@example.com"
        viewModel.password = "password123"

        wait(for: [expectation], timeout: 1.0)
    }
}

This example of Combine testing shows how you can assert on publisher output without manually juggling expectations in multiple places.

FAQ: short, practical answers

What are some real examples of using Combine framework in Swift apps?

Real examples include:

  • Networking pipelines with URLSession.DataTaskPublisher
  • Debounced search bars in UIKit or SwiftUI
  • Form validation using @Published and CombineLatest
  • Observing keyboard or app lifecycle notifications
  • Timer‑driven auto‑refresh for dashboards or feeds

Can you give an example of Combine working with async/await?

Yes. A common example of using Combine framework in Swift with async/await is bridging an existing publisher‑based API into an async function using withCheckedThrowingContinuation, like the firstValue() helper shown above. That lets older Combine code feed newer async call sites without a rewrite.

Is Combine still worth learning in 2024–2025?

If you work on iOS or macOS and care about reactive streams—search, live updates, notifications—then yes. Apple still uses Combine under the hood for SwiftUI and related frameworks. For one‑shot tasks, async/await is simpler, but for streams, the best examples of production code still lean heavily on Combine.

Where can I learn more about reactive patterns that inspired Combine?

Combine’s design is heavily influenced by Reactive Streams and functional reactive programming. While Apple’s own documentation on developer.apple.com is the place to start, you can also explore general functional programming concepts from academic sources like MIT OpenCourseWare or Stanford’s CS courses.


The short version: if you remember nothing else from these examples of using Combine framework in Swift, remember this pattern:

  • Treat UI and network as streams of values
  • Express your logic as transformations on those streams
  • Keep view models thin and testable using publishers

Once you start thinking that way, Combine stops feeling abstract and starts feeling like an honest upgrade over callback soup.

Explore More Swift Code Snippets

Discover more examples and insights in this category.

View All Swift Code Snippets