All articles
iOS15 min read

SwiftUI with Combine: Real-Time Data, Publishers & Subscribers Explained

This guide will help you master reactive programming in SwiftUI — with real examples, how Combine fits with SwiftUI, and when to consider…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🧠 Introduction: Why SwiftUI Still Needs Combine (Sometimes)

SwiftUI is powerful — you write a few lines of code, and your UI just works. But when your app needs real-time updates, debounced searches, or live data streams, you need something more reactive under the hood. That’s where Combine steps in. 🧩

But isn’t async/await the future?

It is. And it’s perfect for one-time asynchronous tasks — like fetching data or saving a file. But when you’re dealing with continuous data, such as user input or timer-based UI changes, Combine is still the better tool for the job.

In this article, you’ll learn:

We’ll also cover common mistakes like forgotten subscriptions or unintentional infinite loops — so you don’t end up fighting Combine more than using it.

💡 If you’re new to SwiftUI state management, _this guide on_ [_@State_](https://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d) _vs_ [_@Binding_](https://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d) _vs_ [_@ObservedObject_](https://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d) _vs_ [_@StateObject_](https://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d) is a great place to start.

**Mastering SwiftUI State Management: @State vs @Binding vs @ObservedObject vs @StateObject (2025…** _Confused between @State, @Binding, @ObservedObject, and @StateObject in SwiftUI? You’re not alone. This 2025 guide…_medium.comhttps://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d

Let’s dive into the world of Combine — and see how it still powers some of the smartest SwiftUI apps in 2025.

🧩 1. The Basics of Combine You Actually Need

Let’s be honest — Combine can feel like learning a second programming language. There are publishers, subscribers, operators, subjects, cancellables… and if you’re just trying to show some text when a value changes, it can feel like overkill. But in SwiftUI, you don’t need to learn everything. You just need the right pieces.

📰 Publishers: The data broadcasters

A Publisher is a type that emits a sequence of values over time. Think of it like a news channel that keeps sending updates — only instead of breaking news, it might be sending your search text every time the user types.

Examples of publishers in SwiftUI:

👂 Subscribers: The listeners

A Subscriber is anyone tuning in to the publisher’s broadcast. It receives values, errors, or completion signals and decides what to do with them — update the UI, start a network call, validate a field, etc.

In SwiftUI, you often don’t need to write custom subscribers. When you use @ObservedObject, SwiftUI does the subscribing for you.

🔁 The Combine Pipeline

The magic happens when you connect a publisher to a subscriber through a chain of operators — like .map, .filter, .debounce, or .removeDuplicates. This chain is called a pipeline, and it transforms the data as it flows.

Example:

$searchText
  .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
  .removeDuplicates()
  .sink { query in
      print("Search for: \(query)")
  }

This says: “Wait for 300ms of no typing, ignore repeats, and then print the final input.”

👉 Don’t forget to store the result of .sink(...) — that’s your Cancellable, and without it, the whole pipeline gets canceled instantly. We’ll get into this more in the Pitfalls section.

📣 Subjects (Optional, but Handy)

Subjects are like publishers and subscribers rolled into one. You can send values manually using something like PassthroughSubject<String, Never>() — useful for things like programmatic event handling or bridging non-Combine code.

Coming up next: we’ll see how all this fits naturally into SwiftUI, with @Published, @ObservedObject, and some sweet real-time UI updates.

🖇️ 2. Combine + SwiftUI: How They Work Together

This is where the magic happens — when Combine and SwiftUI start talking to each other like old friends.

👨‍🏫 Enter: @Published and @ObservedObject

If you’ve used @ObservedObject in SwiftUI, congrats — you’ve already used Combine.

Here’s the connection:

Put them together, and you’ve got reactive updates — every time the @Published property changes, SwiftUI gets the memo and re-renders the UI.

Let’s see it in action 👇

🔄 Simple Example: TextField that updates a label in real time

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
}
struct SearchView: View {
    @StateObject private var viewModel = SearchViewModel()

    var body: some View {
        VStack {
            TextField("Search", text: $viewModel.searchText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Text("You typed: \(viewModel.searchText)")
        }
        .padding()
    }
}

Every time the user types, the @Published property emits a new value, and SwiftUI updates the label. You didn’t need to manually bind any data or call reload.

Want to understand how this data flow works under the hood? Check out this guide on SwiftUI state management, which dives deeper into @State, @Binding, @ObservedObject, and more.

**Mastering SwiftUI State Management: @State vs @Binding vs @ObservedObject vs @StateObject (2025…** _Confused between @State, @Binding, @ObservedObject, and @StateObject in SwiftUI? You’re not alone. This 2025 guide…_medium.comhttps://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d

🧠 What SwiftUI Does For You

Behind the scenes, SwiftUI:

This built-in integration is what makes SwiftUI + Combine such a clean pairing. It’s like SwiftUI was born to react — you just need to give it something worth reacting to.

Next up: Let’s take this from “neat” to “actually useful” with a real-world example — a debounced search bar powered by Combine.

🧪 3. Real-World Example: Debounced Search Bar with Combine

Let’s build something practical: a search bar that waits for the user to stop typing before triggering a search. This avoids unnecessary API calls and gives a smoother user experience — a classic use case for Combine.

🧱 Step 1: ViewModel with Debounce Logic

We’ll use @Published for the raw input, apply debounce and removeDuplicates, and then perform the search.

import Combine

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var results: [String] = []

    private var cancellables = Set<AnyCancellable>()

    init() {
        $searchText
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
            .sink { [weak self] text in
                self?.performSearch(for: text)
            }
            .store(in: &cancellables)
    }

    private func performSearch(for query: String) {
        // Simulated search logic
        let mockData = ["Swift", "SwiftUI", "Combine", "Async/Await", "Concurrency"]
        results = mockData.filter { $0.lowercased().contains(query.lowercased()) }
    }
}

🧱 Step 2: SwiftUI View with Live Updates

struct SearchView: View {
    @StateObject private var viewModel = SearchViewModel()

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            TextField("Search", text: $viewModel.searchText)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Text("Results:")
                .font(.headline)

            ForEach(viewModel.results, id: \.self) { result in
                Text(result)
            }
        }
        .padding()
    }
}

✅ What This Does:

This pattern is especially useful for search UIs, form validation, auto-saving, and anything that requires reacting to changes without overwhelming the system with events.

In the next section, we’ll contrast this approach with async/await — so you can see when to use Combine and when to reach for structured concurrency instead.

🔀 4. Combine vs async/await in SwiftUI: When to Use What

With async/await now firmly part of Swift, you might wonder: Do we still need Combine at all?

The answer: Yes, but only where it fits.

Let’s break it down.

🧭 Use Combine When:

🧭 Use async/await When:

⚔️ Side-by-Side: SearchBar Example with async/await

Here’s how that same debounced search could look using async/await and a Task:

class SearchViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var results: [String] = []

    private var searchTask: Task<Void, Never>?

    func search() {
        searchTask?.cancel()

        searchTask = Task { [weak self] in
            try await Task.sleep(nanoseconds: 500_000_000) // 0.5 sec debounce

            guard let self = self else { return }
            let query = self.searchText

            let mockData = ["Swift", "SwiftUI", "Combine", "Async/Await", "Concurrency"]
            let filtered = mockData.filter { $0.lowercased().contains(query.lowercased()) }

            await MainActor.run {
                self.results = filtered
            }
        }
    }
}
struct SearchView: View {
    @StateObject private var viewModel = SearchViewModel()

    var body: some View {
        VStack {
            TextField("Search", text: $viewModel.searchText)
                .textFieldStyle(.roundedBorder)
                .onChange(of: viewModel.searchText) { _ in
                    viewModel.search()
                }

            ForEach(viewModel.results, id: \.self) {
                Text($0)
            }
        }
        .padding()
    }
}

💡 Which One Should You Choose?

🧠 For a deeper dive into async/await patterns, check out _this practical guide on real-world usage_.

**Mastering async/await in Swift: Real-World Examples for iOS Developers (Part 2)** _Learn how to use async/await in your actual iOS projects — from network calls and image loading to chaining and…_medium.comhttps://medium.com/swift-pal/mastering-async-await-in-swift-real-world-examples-for-ios-developers-part-2-b3e43b1c27e7

Next up: we’ll look at common Combine pitfalls that catch even experienced devs off guard.

🧯 5. Common Pitfalls and Gotchas

Combine is powerful, but it’s also easy to misuse — especially when mixed with SwiftUI. Here are the most common mistakes developers run into (and how to avoid them):

🕳️ 1. Forgetting to Store Subscriptions

If you use .sink { ... } or .assign(to:on:) and don’t store the resulting cancellable, your subscription will get cancelled immediately — and nothing will work.

✅ Always store it:

private var cancellables = Set<AnyCancellable>()

publisher
  .sink { _ in }
  .store(in: &cancellables)

If you forget this, your pipeline will silently fail.

🔁 2. Creating Infinite Loops

This often happens when:

❌ Bad pattern:

$searchText
  .sink { self.searchText = $0.uppercased() }
  .store(in: &cancellables)

This causes an endless feedback loop of updates. ✅ Always check if the new value is different or use .removeDuplicates().

🧠 3. Using @Published in the Wrong Context

@Published only works inside a class that conforms to **ObservableObject**. Using it inside a struct or a class that doesn’t conform won’t trigger SwiftUI updates.

🚨 4. Performing Side Effects Inside the Combine Chain

Avoid performing side effects (like UI updates or API calls) directly inside operators like **.map** or **.filter**.

✅ Right way: Do your work inside .sink, which is meant for side effects.

📉 5. Memory Leaks from Strong Reference Cycles

If you capture self strongly inside a Combine closure, you risk creating a retain cycle — especially if self holds on to the cancellable set.

✅ Always use [weak self] in your sinks:

.sink { [weak self] value in
    self?.doSomething(with: value)
}

📍 Reference If Needed:

If you’re also using structured concurrency, check out this guide on Swift concurrency vs GCD vs Operations to understand where Combine fits in your overall async strategy.

**GCD vs Operations vs Swift Concurrency — Made Easy for iOS Engineers** _Confused about when to use GCD, OperationQueue, or Swift Concurrency in your iOS projects? This guide breaks it down…_medium.comhttps://medium.com/swift-pal/gcd-vs-operations-vs-swift-concurrency-made-easy-for-ios-engineers-e4504e992291

Next, we’ll look at an advanced bonus: how to create custom Combine publishers for things like reachability, keyboard events, or even your own app-level signals.

📦 6. Bonus: Custom Publishers in SwiftUI Projects

Combine isn’t just about system-provided publishers or @Published properties. You can also create your own custom publishers — which gives you a flexible way to react to events outside of SwiftUI’s default system.

This is useful when:

🛠 Example: Custom Publisher for Keyboard Height

Let’s say you want to observe the keyboard’s frame height and adjust your layout accordingly.

import Combine
import UIKit

extension Publishers {
    static var keyboardHeight: AnyPublisher<CGFloat, Never> {
        let willShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
            .map { notification -> CGFloat in
                let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
                return frame?.height ?? 0
            }

        let willHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .map { _ in CGFloat(0) }

        return MergeMany(willShow, willHide)
            .eraseToAnyPublisher()
    }
}

You can now subscribe to this publisher inside your ViewModel or even from a SwiftUI view (though for views, you’d usually forward it through @ObservedObject).

Example Usage: Use it in a ViewModel with @Published

class KeyboardAwareViewModel: ObservableObject {
    @Published var keyboardHeight: CGFloat = 0

    private var cancellables = Set<AnyCancellable>()

    init() {
        Publishers.keyboardHeight
            .receive(on: RunLoop.main)
            .assign(to: \.keyboardHeight, on: self)
            .store(in: &cancellables)
    }
}

Other Useful Custom Publisher Ideas:

Creating these publishers helps separate concerns, makes your code more testable, and keeps your SwiftUI views declarative and clean.

If you’re building more advanced features like this, you’re likely thinking about architecture. Here’s a guide that complements this topic well: 👉 MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture

**MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture** _Let’s unravel the mystery of MVVM and learn how to structure your SwiftUI apps with clean, testable architecture…_medium.comhttps://medium.com/swift-pal/mvvm-in-swiftui-explained-build-scalable-testable-apps-with-clean-architecture-2b36443ebfca

✅ Final Thoughts: When Combine Still Shines

SwiftUI makes a lot of things easier — and async/await simplified one-shot async operations even further. But when you're dealing with continuous streams of values, reactive UI logic, or integrating with legacy APIs, Combine still fits naturally into modern SwiftUI projects.

To recap, you’ve learned:

📅 Coming Up Next in the SwiftUI Mastery Series:

_SwiftUI with Async/Await: Modern Networking_Learn how to build fast, modern, and testable networking layers using Swift’s concurrency model, _URLSession_, and _Task_. We'll compare it with Combine and handle everything from API fetching to error handling.

🧭 Where to Go From Here

If you want to keep building modern, reactive, and testable SwiftUI apps — here are some handpicked next reads from the Swift Pal collection:

👉 Mastering Async/Await in Swift — Real World Examples 👉 SwiftUI State Management: @State vs @Binding vs @ObservedObject 👉 MVVM in SwiftUI Explained: Clean Architecture You Can Scale

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading