Swift Combine Series: Part 3 — Mastering Combine and SwiftUI Integration
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.
“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 theCounterViewModel
automatically notifies SwiftUI whencount
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!
What’s Next?
In Part 4, we’ll delve into Combine’s advanced operators, including flatMap
, switchToLatest
, and custom publishers. Don’t miss it—it’s where the real magic happens!