Swift Combine Series: Part 3 — Mastering Combine and SwiftUI Integration

Vikram Kumar
3 min readJan 26, 2025

--

Welcome to Part 3 of the Swift Combine series! In this installment, we’ll dive into the seamless integration of Combine and SwiftUI. If you thought Combine was already magical, wait until you see how beautifully it syncs with SwiftUI’s declarative approach.

Grab your coding cape, because by the end of this article, you’ll be ready to create reactive, data-driven UIs with confidence.

Photo by Antonio Janeski on Unsplash

“SwiftUI and Combine are like peanut butter and jelly — better together.”

With Combine, you unlock the full power of reactive programming, while SwiftUI takes care of making everything look stunning.

Why Combine with SwiftUI?

SwiftUI and Combine were designed to complement each other. While SwiftUI focuses on UI composition, Combine handles the underlying data flow and logic. Together, they:

  • Eliminate Boilerplate: No more manual bindings.
  • Update UI Automatically: Data changes update your UI instantly.
  • Promote Reactive Thinking: Streamline your app’s architecture.

Binding State with @Published and @ObservedObject

SwiftUI leverages Combine’s publishers to drive UI updates. Let’s start with an example of using @Published and @ObservedObject.

Example 1: A Simple Counter App

import SwiftUI
import Combine

class CounterViewModel: ObservableObject {
@Published var count = 0

func increment() {
count += 1
}

func decrement() {
count -= 1
}
}

struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel()

var body: some View {
VStack {
Text("Count: \(viewModel.count)")
.font(.largeTitle)

HStack {
Button("-") {
viewModel.decrement()
}
.padding()

Button("+") {
viewModel.increment()
}
.padding()
}
}
.padding()
}
}

struct ContentView: View {
var body: some View {
CounterView()
}
}

@main
struct CombineSwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

What Happens Here?

  • @Published in the CounterViewModel automatically notifies SwiftUI when count changes.
  • The Text view updates in real time.

Using Combine to Fetch Data in SwiftUI

Let’s create a basic example where we fetch posts from an API and display them in a SwiftUI list.

Example 2: Fetching Data with Combine

import SwiftUI
import Combine

class PostsViewModel: ObservableObject {
@Published var posts: [Post] = []
private var cancellables = Set<AnyCancellable>()

func fetchPosts() {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!

URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [Post].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error fetching posts: \(error)")
}
}, receiveValue: { [weak self] posts in
self?.posts = posts
})
.store(in: &cancellables)
}
}

struct Post: Identifiable, Decodable {
let id: Int
let title: String
let body: String
}

struct PostsListView: View {
@StateObject var viewModel = PostsViewModel()

var body: some View {
NavigationView {
List(viewModel.posts) { post in
VStack(alignment: .leading) {
Text(post.title).font(.headline)
Text(post.body).font(.subheadline)
}
}
.navigationTitle("Posts")
.onAppear {
viewModel.fetchPosts()
}
}
}
}

@main
struct CombineSwiftUIApp: App {
var body: some Scene {
WindowGroup {
PostsListView()
}
}
}

Key Points:

  • The PostsViewModel uses Combine to fetch data.
  • @StateObject ensures the view model stays alive as long as the view exists.
  • Data is fetched and displayed dynamically in the List.

Combining Multiple Publishers

What if your app needs to merge data from multiple sources? Combine makes this simple.

Example 3: Merging Data Streams

class MultiSourceViewModel: ObservableObject {
@Published var combinedData: String = ""
private var cancellables = Set<AnyCancellable>()

func fetchData() {
let publisher1 = Just("Hello")
let publisher2 = Just(", Combine!")

Publishers.CombineLatest(publisher1, publisher2)
.map { $0 + $1 }
.receive(on: DispatchQueue.main)
.sink { [weak self] combined in
self?.combinedData = combined
}
.store(in: &cancellables)
}
}

struct MultiSourceView: View {
@StateObject var viewModel = MultiSourceViewModel()

var body: some View {
Text(viewModel.combinedData)
.onAppear {
viewModel.fetchData()
}
.padding()
}
}

Output:

Hello, Combine!

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

Responses (1)