Practical examples of examples of using Combine framework in Swift
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
@PublishedandCombineLatest - 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.
Related Topics
Practical examples of simple calculator app examples in Swift
Practical examples of Swift async/await examples for asynchronous programming
Best examples of practical examples of using closures in Swift
Practical examples of UITableView implementation in Swift for modern iOS apps
Practical examples of timer examples with Grand Central Dispatch in Swift
The best examples of error handling in Swift: 3 practical examples you’ll actually use
Explore More Swift Code Snippets
Discover more examples and insights in this category.
View All Swift Code Snippets