Network programming is essential for modern iOS apps. Learn to make HTTP requests, handle errors, and build robust network layers.
URLSession Basics
Simple GET Request
swift
import Foundation
struct User: Codable {
let id: Int
let name: String
let email: String
}
class NetworkManager {
func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([User].self, from: data)
}
}POST Request
swift
struct Post: Codable {
let title: String
let body: String
let userId: Int
}
class NetworkManager {
func createPost(title: String, body: String) async throws -> Post {
let url = URL(string: "https://api.example.com/posts")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let post = Post(title: title, body: body, userId: 1)
request.httpBody = try JSONEncoder().encode(post)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 201 else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(Post.self, from: data)
}
}
enum NetworkError: Error {
case invalidURL
case invalidResponse
case decodingFailed
}Error Handling
Comprehensive Error Handling
swift
enum NetworkError: Error, LocalizedError {
case invalidURL
case requestFailed(Error)
case invalidResponse
case httpError(statusCode: Int)
case decodingFailed(Error)
case encodingFailed(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL provided"
case .requestFailed(let error):
return "Request failed: \(error.localizedDescription)"
case .invalidResponse:
return "Invalid response from server"
case .httpError(let statusCode):
return "HTTP error with status code: \(statusCode)"
case .decodingFailed(let error):
return "Decoding failed: \(error.localizedDescription)"
case .encodingFailed(let error):
return "Encoding failed: \(error.localizedDescription)"
}
}
}
class NetworkManager {
func fetchData<T: Decodable>(from url: URL) async throws -> T {
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(statusCode: httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed(error)
}
} catch let error as NetworkError {
throw error
} catch {
throw NetworkError.requestFailed(error)
}
}
}Request Configuration
Custom URLRequest
swift
struct URLRequestBuilder {
let baseURL: URL
var headers: [String: String] = [:]
var timeout: TimeInterval = 30
var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
func build(path: String, method: HTTPMethod = .get) -> URLRequest {
var url = baseURL.appendingPathComponent(path)
// Add query parameters for GET requests
if method == .get, !queryParameters.isEmpty {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.queryItems = queryParameters.map { key, value in
URLQueryItem(name: key, value: value)
}
url = components.url!
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.timeoutInterval = timeout
request.cachePolicy = cachePolicy
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
return request
}
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}Authentication
Bearer Token
swift
class AuthenticatedNetworkManager {
private let tokenManager: TokenManager
init(tokenManager: TokenManager) {
self.tokenManager = tokenManager
}
func request<T: Decodable>(
_ url: URL,
method: HTTPMethod = .get
) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
// Add bearer token
let token = try await tokenManager.getCurrentToken()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
// Handle token expiration
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 401 {
try await tokenManager.refreshToken()
return try await self.request(url, method: method)
}
return try JSONDecoder().decode(T.self, from: data)
}
}API Keys
swift
struct APIConfiguration {
let apiKey: String
let baseURL: URL
static let defaultConfig = APIConfiguration(
apiKey: "your-api-key",
baseURL: URL(string: "https://api.example.com")!
)
}
class NetworkManager {
let configuration: APIConfiguration
init(configuration: APIConfiguration = .default) {
self.configuration = configuration
}
func createRequest(path: String) -> URLRequest {
var request = URLRequest(url: configuration.baseURL.appendingPathComponent(path))
request.setValue(configuration.apiKey, forHTTPHeaderField: "X-API-Key")
return request
}
}Image Loading
Async Image Loading
swift
class ImageLoader: ObservableObject {
@Published var image: Image?
@Published var isLoading = false
private let cache: ImageCache
private let urlSession: URLSession
init(cache: ImageCache = .shared, urlSession: URLSession = .shared) {
self.cache = cache
self.urlSession = urlSession
}
func loadImage(from url: URL) async {
isLoading = true
defer { isLoading = false }
// Check cache first
if let cached = await cache.getImage(for: url) {
self.image = cached
return
}
// Download image
do {
let (data, _) = try await urlSession.data(from: url)
if let uiImage = UIImage(data: data) {
let image = Image(uiImage: uiImage)
self.image = image
await cache.setImage(image, for: url)
}
} catch {
print("Failed to load image: \(error)")
}
}
}
actor ImageCache {
static let shared = ImageCache()
private var storage: [URL: Image] = [:]
func getImage(for url: URL) -> Image? {
storage[url]
}
func setImage(_ image: Image, for url: URL) {
storage[url] = image
}
}Networking Layer Architecture
Protocol-Based Design
swift
protocol NetworkServiceProtocol {
func request<T: Decodable>(
_ endpoint: Endpoint,
responseType: T.Type
) async throws -> T
}
enum Endpoint {
case users
case user(id: Int)
case posts
case post(id: Int)
var path: String {
switch self {
case .users:
return "/users"
case .user(let id):
return "/users/\(id)"
case .posts:
return "/posts"
case .post(let id):
return "/posts/\(id)"
}
}
}
class NetworkService: NetworkServiceProtocol {
private let baseURL: URL
private let session: URLSession
init(baseURL: URL, session: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
}
func request<T: Decodable>(
_ endpoint: Endpoint,
responseType: T.Type
) async throws -> T {
let url = baseURL.appendingPathComponent(endpoint.path)
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(T.self, from: data)
}
}
// Usage
let service = NetworkService(
baseURL: URL(string: "https://jsonplaceholder.typicode.com")!
)
let users: [User] = try await service.request(.users, responseType: [User].self)Best Practices
- 1Use async/await: Modern concurrency for network calls
- 2Error handling: Comprehensive error handling and recovery
- 3Caching: Cache responses to reduce network calls
- 4Authentication: Secure token management
- 5Request retry: Implement retry logic for failed requests
- 6Offline support: Handle offline scenarios gracefully
- 7Logging: Log network requests for debugging
- 8Testing: Mock network responses for testing
Network programming is crucial for connected iOS apps. Build robust and efficient network layers!