-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
126: Practice Exercise: Building a Weather App with Combine
We've spent a few lessons talking about the individual building blocks of Combine—publishers, subscribers, and operators. But honestly, Combine doesn't really "click" until you see it managing a real data flow from a network request all the way to the UI. So, let's build a miniature weather tracker. We aren't going to overcomplicate this with a massive API integration; we'll focus on the pipeline.
Modeling the Weather Response
First, I need a data structure that matches what a weather API typically returns. I'm keeping this lean. We only care about the temperature and the condition description. Since we're using JSONDecoder later in the pipeline, I'll make sure these conform to Codable.
struct Weather: Codable {
let temperature: Double
let condition: String
let cityName: String
}
Wiring up the Data Stream
Now for the meat of the logic. I like to keep my network logic in a dedicated service. Instead of using the old completion handler pattern, I'm going to use dataTaskPublisher. It's a built-in Combine publisher that makes the network request and hands us a tuple of data and a response.
I'll chain a few operators here: map to grab just the data, and decode to turn that JSON into our Weather struct. I'm using eraseToAnyPublisher() at the end because I don't want the ViewModel to care about the complex nested type the chain produces.
class WeatherService {
func fetchWeather(for city: String) -> AnyPublisher<Weather, Error> {
let url = URL(string: "https://api.example.com/weather?q=\(city)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Weather.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
Managing State in the ViewModel
This is where we actually "listen" to the stream. I'll create a WeatherViewModel that holds a @Published property for the weather. When the view asks for an update, the ViewModel will subscribe to the service.
I'm using a Set<AnyCancellable> to keep the subscription alive. If I don't store the cancellable, the request will be cancelled the moment the function finishes executing, and you'll wonder why your UI never updates.
class WeatherViewModel: ObservableObject {
@Published var weather: Weather?
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
private let service = WeatherService()
func updateWeather(city: String) {
service.fetchWeather(for: city)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
self.errorMessage = error.localizedDescription
}
}, receiveValue: { weather in
self.weather = weather
})
.store(in: &cancellables)
}
}
The "Main Thread" Gotcha
I actually ran this code just now, and it crashed. Why? Because dataTaskPublisher performs its work on a background thread. When the sink receives the value and tries to update self.weather, it's doing so on that background thread. Since @Published triggers a UI update, SwiftUI throws a fit because UI changes must happen on the main thread.
I forgot the most important operator for network calls: receive(on:). I need to tell Combine to hop back to the main queue before hitting the subscriber. Here is the corrected updateWeather method:
func updateWeather(city: String) {
service.fetchWeather(for: city)
.receive(on: RunLoop.main) // Added this to fix the threading crash
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
self.errorMessage = error.localizedDescription
}
}, receiveValue: { weather in
self.weather = weather
})
.store(in: &cancellables)
}
Connecting to the View
With the pipeline finished, the SwiftUI side is trivial. The view just observes the ViewModel. When weather changes, the view re-renders. It's a clean, unidirectional flow: View triggers action → Service fetches data → Combine processes stream → ViewModel updates state → View reflects change.
📋 Practical Task
Exercise: Adding a Temperature Unit Toggle
Currently, our weather app only shows the temperature as it comes from the API. Your task is to implement a unit conversion feature using Combine.
- Add a
@Published var useFahrenheit: Bool = falseproperty to theWeatherViewModel. - Create a new
@Published var displayTemperature: String = ""property to hold the formatted string. - In the ViewModel's initializer, use the
combineLatestoperator to listen to both the$weatherpublisher and the$useFahrenheitpublisher. - Whenever either changes, calculate the correct temperature (Celsius to Fahrenheit conversion:
(C * 9/5) + 32) and update thedisplayTemperaturestring. - Ensure the final subscription is stored in the
cancellablesset and updates on the main thread.
There are no comments for now.