Swift
Completed
-
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
83: Combine Publishers from URLSession
When you first start mixing URLSession with Combine, there's a temptation to stick with what you know. Most of us spent years writing completion handlers, so the natural instinct is to wrap that old-school dataTask in a Future or a PassthroughSubject. I've seen a lot of developers do this because it feels like a safe bridge between the imperative world and the reactive one.
The Manual Bridge
Imagine we're building a tool to fetch a list of public repositories for a GitHub user. The "naive" way looks something like this: you create a Future, call dataTask(with:completionHandler:), and manually call the promise's resolve or reject based on the response. It looks a bit like this:
func fetchReposNaive(for user: String) -> AnyPublisher<[Repo], Error> {
Future { promise in
let url = URL(string: "https://api.github.com/users/\(user)/repos")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
promise(.failure(error))
return
}
// Now you're stuck doing manual JSON decoding inside a closure...
do {
let repos = try JSONDecoder().decode([Repo].self, from: data!)
promise(.success(repos))
} catch {
promise(.failure(error))
}
}.resume()
}.eraseToAnyPublisher()
}
Here is the problem: you're fighting the framework. You've essentially just wrapped a callback in a fancy box. You're still managing the .resume() call manually, and you're nesting your decoding logic inside a closure, which defeats the whole purpose of Combine's linear, declarative pipelines. It's verbose, and it's where bugs—like forgetting to call the promise in one of the edge cases—tend to hide.
The Native Publisher Approach
Apple realized this was a pain point, which is why URLSession has a built-in publisher: dataTaskPublisher(for:). Instead of you managing the lifecycle of the task, the publisher handles it. The moment you subscribe, the network request starts. When the request finishes, it emits a tuple containing the data and the response.
Let's rewrite that GitHub fetcher. This time, we'll treat the network request as just another link in the chain:
func fetchRepos(for user: String) -> AnyPublisher<[Repo], Error> {
let url = URL(string: "https://api.github.com/users/\(user)/repos")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // We only care about the Data, not the URLResponse
.decode(type: [Repo].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
Notice how much "noise" disappeared. We aren't manually checking for nil data or calling .resume(). We're just describing the transformation: "Take the network output, grab the data, decode it into an array of Repos, and pass it along." It's much easier to reason about because it reads like a sentence.
Taming the Background Thread
There is one major catch I want to warn you about. dataTaskPublisher performs its work on a background thread. If you try to assign the result of this publisher directly to a @Published property that updates a UI element, your app will crash or throw a purple warning in Xcode. You're trying to update the UI from a background queue, which is a cardinal sin in iOS development.
To fix this, you need to explicitly tell Combine to move the result back to the main thread before it reaches the subscriber. I usually drop a .receive(on: DispatchQueue.main) right before the sink or the assign call. If you're wrapping this in a ViewModel, it looks like this:
service.fetchRepos(for: "apple")
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in }, receiveValue: { self.repositories = $0 })
.store(in: &cancellables)
By doing this, you keep the heavy lifting (the network request and the JSON decoding) off the main thread, but ensure the UI update happens safely. It's a clean separation of concerns that doesn't require you to nest DispatchQueue.main.async blocks inside your logic.
📋 Practical Task
Build a GitHub User Profile Fetcher
Create a small program that fetches a single GitHub user's profile data. You will need to define a User struct that conforms to Codable (at minimum, include login and public_repos as properties).
Requirements:
- Use
URLSession.shared.dataTaskPublisher(for:)to hit thehttps://api.github.com/users/{username}endpoint. - Use
.map(\.data)to extract the raw data. - Use
.decode(type:decoder:)to convert the JSON into yourUserstruct. - Handle errors using
.replaceError(with:)to provide a "Guest User" fallback object so the pipeline doesn't fail completely. - Print the user's login name to the console using
.sink.
There are no comments for now.