Using Combine With SwiftUI
Written by Team Kodeco
Combine is a powerful declarative Swift API for processing values over time. It provides a unified approach to handle many types of asynchronous and event-driven code. When coupled with SwiftUI, you can create highly responsive UIs that reflect the changing state of your underlying data. In this tutorial, you’ll create a real-time weather application that demonstrates the capabilities of Combine.
Defining a Weather Model
The Combine framework allows you to work with publishers, subscribers and operators. It can be utilized in various scenarios, such as network requests, user input handling and more.
In this example, you’ll create a weather model and a weather service to fetch data.
import Combine
struct Weather: Codable {
let temperature: Double
let description: Description
enum Description: String, Codable, CaseIterable {
case sunny, cloudy, rainy, snowing
}
}
class WeatherService {
func fetchWeather(for city: String) -> AnyPublisher<Weather, Error> {
// Simulating a network call to get weather information
let weather = Weather(
temperature: Double.random(in: 0..<40),
description: Weather.Description.allCases.randomElement()!
)
return Just(weather)
.setFailureType(to: Error.self)
.delay(for: 1.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
View Model with Combine
The view model is where Combine shines. Here, you will use a PassthroughSubject
to receive user input and a Published
property to notify the view of changes.
import SwiftUI
import Combine
class WeatherViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private let weatherService = WeatherService()
@Published var weather: Weather?
let citySubject = PassthroughSubject<String, Never>()
init() {
citySubject
.debounce(for: 0.5, scheduler: RunLoop.main)
.flatMap { [unowned self] city in
self.weatherService.fetchWeather(for: city)
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in }, receiveValue: { [unowned self] weather in
self.weather = weather
})
.store(in: &cancellables)
}
}
Let’s break down the code above, beginning with the definition of the class:
-
cancellables
: A collection that stores references to the Combine subscriptions. This prevents the subscriptions from being deallocated, so they continue receiving updates. -
weatherService
: Your weather service to fetch weather data. -
@Published var weather
: A special property wrapper that automatically notifies the view of changes to the weather data. -
citySubject
: A subject that receives user input. A subject in Combine allows you to inject values into a Combine pipeline.
Next, in the init
:
-
citySubject
: This is where the chain begins. Whenever the user types a city, you send that string to this subject. -
.debounce(for: 0.5, scheduler: RunLoop.main)
: This operator waits until the user stops typing for 0.5 seconds before sending the value along. This avoids making a service call for every keystroke. -
flatMap
: This operator takes the city string and calls your weather service’s method, which returns a publisher. If multiple values are sent quickly,flatMap
can handle them without waiting for the previous ones to complete. -
.receive(on: RunLoop.main)
: This ensures that the code in the subsequentsink
operator runs on the main thread. This is essential when updating the UI. -
sink
: This terminal operator catches the value and allows us to use it. Here, you update the@Published
weather property, which triggers a UI refresh. -
.store(in: &cancellables)
: This stores the subscription in your cancellables set so that it continues to receive updates.
SwiftUI View
Now, let’s create a view that interacts with our view model.
struct ContentView: View {
@ObservedObject var viewModel = WeatherViewModel()
@State private var city: String = ""
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Enter city", text: $city, onCommit: {
viewModel.citySubject.send(city)
})
if let weather = viewModel.weather {
HStack {
Image(systemName: imageName(for: weather.description))
VStack(alignment: .leading) {
Text("Temperature: \(Int(weather.temperature.rounded()))°C")
.font(.headline)
Text(weather.description.rawValue.capitalized)
.font(.subheadline)
}
}
}
}
.padding()
}
func imageName(for description: Weather.Description) -> String {
switch description {
case .sunny: return "sun.max.fill"
case .cloudy: return "cloud.fill"
case .rainy: return "cloud.rain.fill"
case .snowing: return "cloud.snow.fill"
}
}
}
Here’s what your preview should look like:
The view observes the @Published
property of the ViewModel and updates the UI accordingly. The TextField
is used to receive user input, and the onCommit
closure is used to send the city name to the ViewModel.
Using the Combine framework with SwiftUI, you have created a live weather app that responds to user input in real time. The Combine framework provides a way to streamline asynchronous and event-driven code, making it easier to write, read, and maintain. This example can be further expanded with real network calls, error handling, and more advanced UI. Try running it in Xcode and have fun exploring Combine’s powerful features!