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
52: Building a Simple Networking Layer
When you first start hitting APIs in Swift, the temptation is to just throw a URLSession call right into your ViewModel or, heaven forbid, your ViewController. It feels efficient at the time. You just want the data on the screen, and URLSession.shared.dataTask is right there, ready to go. But as your app grows from one screen to ten, this "quick and dirty" approach becomes a maintenance nightmare.
The "Just Get It Working" Approach
Imagine we're building an app that tracks SpaceX launches. In a naive implementation, you might write a function in your ViewModel that looks something like this:
func fetchLaunches() async {
let url = URL(string: "https://api.spacexdata.com/v4/launches")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
let launches = try JSONDecoder().decode([Launch].self, from: data)
self.launches = launches
} catch {
print("Something went wrong: \(error)")
}
}
On the surface, this works. It's concise. But I've seen this pattern kill productivity in larger projects because it mixes what the app is doing (fetching launches) with how it's doing it (URLSession, JSONDecoder, specific URLs). You're essentially hard-coding your networking logic into your business logic.
Where This Breaks
The moment you need to fetch "Latest Launch" or "Company Details," you'll find yourself copying and pasting that do-catch block over and over. If you suddenly need to add an API key to every request header, you're hunting through fifteen different files to update a string. Worse, testing becomes a chore. Since you're calling URLSession.shared directly, you can't easily swap the real network for a mock one during a unit test without some very ugly hacking.
Building a Scalable Client
The better way is to separate the definition of your API from the execution of the request. I usually start by creating an Endpoint abstraction. Instead of scattering strings everywhere, we use an enum to define our routes. This gives us a single place to manage URLs and HTTP methods.
enum SpaceXEndpoint {
case launches
case latestLaunch
var url: URL {
switch self {
case .launches: return URL(string: "https://api.spacexdata.com/v4/launches")!
case .latestLaunch: return URL(string: "https://api.spacexdata.com/v4/launches/latest")!
}
}
}
Now, we build a generic NetworkClient. The key here is using a generic type T: Decodable. I don't want my networking layer to know what a "Launch" is; I just want it to know how to fetch data and turn it into something that conforms to Decodable.
protocol NetworkClientProtocol {
func request<T: Decodable>(_ endpoint: SpaceXEndpoint) async throws -> T
}
class NetworkClient: NetworkClientProtocol {
func request<T: Decodable>(_ endpoint: SpaceXEndpoint) async throws -> T {
let (data, response) = try await URLSession.shared.data(from: endpoint.url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
}
}
By doing this, your ViewModel becomes blissfully ignorant of the networking plumbing. It just says, "Hey client, give me the launches," and it gets a typed array back. If you ever decide to switch from URLSession to something like Alamofire, or if the API version changes from v4 to v5, you only change one file, not your entire app. I highly recommend using the protocol NetworkClientProtocol here—it allows you to inject a MockNetworkClient during tests so you aren't burning through your API quota or relying on a stable internet connection to verify your UI logic.
📋 Practical Task
Implement a Generic Movie API Client
Build a networking layer for a hypothetical Movie Database API. You will need to implement the following:
- An
MovieEndpointenum that handles two routes:.popularMoviesand.movieDetails(id: Int). Ensure the details route dynamically inserts the ID into the URL string. - A
MovieClientclass that conforms to a protocol. It should feature a genericfetch<T: Decodable>method that handles theURLSessioncall and decodes the response. - A
MovieViewModelthat uses theMovieClientto fetch a list of movies. The ViewModel should not contain anyURLSessionorJSONDecodercode; it should rely entirely on the client.
Requirement: Use async/await for the asynchronous calls and ensure that non-200 HTTP status codes throw a custom error.
There are no comments for now.