Networking with URLSessionIntroductionMost iOS applications depend on network services. A weather application retrieves forecasts, a banking application obtains account information, and a social application uploads photographs and downloads messages. Networking is therefore not an optional feature of modern mobile development; it is a boundary between the application and an external system that may be slow, unavailable, incorrectly configured, or returning unexpected data. Apple’s URLSession framework provides the principal high-level API for HTTP and HTTPS communication in iOS. It supports ordinary requests, uploads, downloads, authentication, caching, background transfers, cancellation, and asynchronous programming. This chapter develops a practical model for constructing requests with URLRequest, executing them with URLSession, decoding JSON responses with Codable, transferring files, and updating the user interface safely. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish a URL, an HTTP request, a response, and a session;
- construct GET, POST, and other requests using
URLRequest; - execute requests with
async/await; - validate HTTP status codes and decode JSON safely;
- upload data and download files;
- handle cancellation, timeouts, connectivity failures, and malformed responses;
- update SwiftUI or UIKit user interfaces on the main actor;
- apply security, privacy, and maintainability practices to network code.
The Network Request ModelA network exchange has four important parts: - URL — identifies the resource, including scheme, host, path, and possibly query items.
- Request — adds an HTTP method, headers, body, and policies such as cache behavior.
- Response — contains status information, headers, and a payload.
- Session — manages connections, authentication, caching, cookies, configuration, and transfer tasks.
For example, a request to https://api.example.com/books?limit=20 can be interpreted as follows: | Component | Meaning |
|---|
https | Secure transport scheme | api.example.com | Server host | /books | Resource path | limit=20 | Query parameter |
HTTP methods communicate intent. GET retrieves data, POST commonly creates a resource or submits an operation, PUT replaces a resource, PATCH partially modifies one, and DELETE removes one. The meaning is ultimately defined by the API contract. A successful transport operation does not necessarily mean a successful application operation. A server may return HTTP 404 or 500 while the request itself was transmitted correctly. Consequently, robust code checks both transport errors and HTTP status codes. Constructing URLRequest ValuesURLRequest is a mutable value that describes an HTTP request. It is preferable to construct URLs with URLComponents rather than concatenating strings, because query values must be correctly percent-encoded.
import Foundation
var components = URLComponents()
components.scheme = "https"
components.host = "api.example.com"
components.path = "/books"
components.queryItems = [
URLQueryItem(name: "limit", value: "20"),
URLQueryItem(name: "category", value: "ios development")
]
guard let url = components.url else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 20A request body is normally used with methods such as POST or PATCH. When sending JSON, the Content-Type header identifies the representation and JSONEncoder performs serialization. struct CreateBook: Encodable {
let title: String
let author: String
}
let newBook = CreateBook(title: "Networking on iOS", author: "A. Rivera")
var postRequest = URLRequest(url: URL(string: "https://api.example.com/books")!)
postRequest.httpMethod = "POST"
postRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
postRequest.setValue("application/json", forHTTPHeaderField: "Accept")
postRequest.httpBody = try JSONEncoder().encode(newBook)In production code, avoid force-unwrapping URLs unless the value is a compile-time constant that has been independently verified. A malformed endpoint should produce a controlled error rather than terminate the application. URLSession and async/awaitThe simplest modern request uses URLSession.shared.data(for:). The method suspends the current asynchronous task while the system performs the transfer; it does not block the calling thread. When the operation completes, it returns (Data, URLResponse) or throws an error. func loadBooks() async throws -> [Book] {
let url = URL(string: "https://api.example.com/books")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw NetworkError.invalidHTTPResponse
}
return try JSONDecoder().decode([Book].self, from: data)
}A useful error model separates URL construction, transport, server, and decoding failures. enum NetworkError: LocalizedError {
case invalidHTTPResponse
case server(statusCode: Int)
case emptyResponse
var errorDescription: String? {
switch self {
case .invalidHTTPResponse:
return "The server returned an invalid response."
case .server(let statusCode):
return "The server returned status code \(statusCode)."
case .emptyResponse:
return "The response contained no data."
}
}
}A more informative validation function preserves the status code: func validate(_ response: URLResponse) throws {
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidHTTPResponse
}
guard 200..<300 ~= http.statusCode else {
throw NetworkError.server(statusCode: http.statusCode)
}
}The expression 200..<300 ~= statusCode tests whether the status code lies in the conventional successful range. APIs may define additional successful codes, so the client should follow the service documentation. Decoding JSON with CodableCodable combines Encodable and Decodable. A model should represent the documented response rather than merely reproducing fields that happen to be present in one sample.
struct Book: Decodable, Identifiable {
let id: Int
let title: String
let authorName: String
enum CodingKeys: String, CodingKey {
case id
case title
case authorName = "author_name"
}
}The decoder converts bytes into typed values, but decoding can fail because of an unexpected key type, missing required field, invalid date, or changed server schema. Date handling must be configured to match the API: let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let books = try decoder.decode([Book].self, from: data) Do not treat decoding failure as proof that the user has no data. It indicates that the client could not interpret the response and should be logged, reported, or shown through an appropriate error state. A Reusable API ClientCentralizing networking reduces duplicated validation and makes testing easier. struct APIClient {
let session: URLSession
let baseURL: URL
init(session: URLSession = .shared,
baseURL: URL = URL(string: "https://api.example.com")!) {
self.session = session
self.baseURL = baseURL
}
func get<T: Decodable>(_ path: String, as type: T.Type) async throws -> T {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
try validate(response)
return try JSONDecoder().decode(T.self, from: data)
}
}Dependency injection of URLSession is important. Tests can use a session configured with a custom URLProtocol, allowing deterministic responses without contacting a real server. Updating the User Interface SafelyUI frameworks require UI state changes on the main actor. In SwiftUI, an observable model can be isolated with @MainActor: @MainActor
final class BooksViewModel: ObservableObject {
@Published private(set) var books: [Book] = []
@Published private(set) var isLoading = false
@Published var errorMessage: String?
private let client = APIClient()
func refresh() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
books = try await client.get("books", as: [Book].self)
} catch is CancellationError {
// Cancellation is normally not presented as a failure.
} catch {
errorMessage = error.localizedDescription
}
}
}A SwiftUI view may start and cancel work according to its lifecycle: struct BooksView: View {
@StateObject private var model = BooksViewModel()
var body: some View {
List(model.books) { book in
VStack(alignment: .leading) {
Text(book.title).font(.headline)
Text(book.authorName)
}
}
.overlay {
if model.isLoading { ProgressView() }
}
.task {
await model.refresh()
}
.refreshable {
await model.refresh()
}
}
}URLSession work is asynchronous, but asynchronous does not mean “automatically safe.” Shared mutable state still needs actor isolation or another synchronization strategy. The view model owns presentation state, while the API client owns communication concerns.
Uploading DataFor a small JSON or binary payload, upload(for:from:) is concise: func createBook(_ book: CreateBook) async throws -> Book {
let url = URL(string: "https://api.example.com/books")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let body = try JSONEncoder().encode(book)
let (data, response) = try await URLSession.shared.upload(
for: request, from: body
)
try validate(response)
return try JSONDecoder().decode(Book.self, from: data)
}Multipart form data is commonly used for files and form fields. Its body contains boundaries, headers, and binary content. The boundary must be unique and must appear consistently in both the Content-Type header and body. For large or repeated uploads, a dedicated multipart builder or a well-tested library can reduce implementation errors. Never place passwords, API keys, or personally identifying data in URLs when the server offers a safer header or body-based mechanism. URLs can appear in logs, browser history, proxy records, or analytics systems. Downloading Data and Filesdata(for:) is appropriate when the response is modest and will be processed in memory. For a file, download(for:) writes the result to a temporary location and returns a file URL, reducing memory pressure.
func downloadPDF() async throws -> URL {
let request = URLRequest(
url: URL(string: "https://api.example.com/report.pdf")!
)
let (temporaryURL, response) = try await URLSession.shared.download(for: request)
try validate(response)
let destination = FileManager.default.temporaryDirectory
.appendingPathComponent("report.pdf")
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: temporaryURL, to: destination)
return destination
}The temporary file must be moved or copied before the system removes it. For user-visible long-running transfers, use a background URLSessionConfiguration and delegate-based progress and completion handling. Background sessions are designed for transfers that may continue when the application is suspended; they involve additional delegate and lifecycle configuration. Cancellation, Timeouts, and RetriesAn asynchronous task can be cancelled when a view disappears or a newer search supersedes an older search. Code should avoid publishing results after cancellation: func search(query: String) async throws -> [Book] {
try Task.checkCancellation()
// Construct and execute request here.
let result: [Book] = try await client.get("search", as: [Book].self)
try Task.checkCancellation()
return result
}Timeouts are not a substitute for good error handling. A timeout expresses how long the client is willing to wait; it does not guarantee that the server stopped processing the request. Automatic retries are appropriate only when the operation is safe to repeat or has idempotent semantics. Retrying a GET is usually less risky than blindly retrying a payment POST. A production retry policy should limit attempts, use increasing delays, and avoid retrying authentication or validation failures. Security, Privacy, and GovernanceUse HTTPS for confidential or authenticated traffic. iOS App Transport Security (ATS) generally requires secure connections and should not be weakened globally to accommodate an improperly configured server. If an exception is genuinely necessary, it should be narrowly scoped, documented, reviewed, and removed when possible. Additional practices include: - validate server certificates through the system trust model rather than accepting all certificates;
- store tokens in Keychain, not in
UserDefaults or source code; - use short-lived access tokens and secure refresh mechanisms;
- minimize transmitted personal data;
- redact tokens and personal information from logs;
- verify authorization on the server, because client-side checks are not security boundaries;
- define retention, deletion, and consent practices for collected data.
Networking also has accessibility and reliability implications. A loading indicator should not prevent users from understanding an error, and a retry action should be explicit where repeated requests could have side effects. Illustrative Case: A Campus Library ApplicationBackground. A campus library application displays books, supports search, and lets students upload a profile photograph. Problem. The first implementation concatenated search strings into URLs, decoded JSON directly in a view, ignored status codes, and updated UI properties from arbitrary completion handlers. It also loaded photographs entirely into memory. Analysis. Search terms containing spaces or symbols produced invalid requests. A server-side 500 response was displayed as an empty list, hiding the real problem. Concurrent searches could finish out of order, causing an older result to replace a newer one. Large images caused memory pressure. Approach. The team introduced an APIClient using URLComponents, URLRequest, and async/await. A @MainActor view model owned loading, success, and error states. Responses were validated before decoding. Search tasks were cancelled when the query changed, and the server endpoint was changed to accept a multipart upload for photographs. A background session was reserved for large document downloads. Outcome. Requests became correctly encoded, server failures became visible and actionable, and UI state was updated predictably. Memory use decreased for large downloads, while tests used a custom session configuration and fixture responses. Learning points. Correct networking is not merely the ability to receive bytes. It requires contract validation, lifecycle management, security controls, cancellation, testability, and clear communication of network state to users. Best Practices Checklist- Build query strings with
URLComponents. - Set the HTTP method and appropriate
Accept and Content-Type headers. - Validate the response type and status code before decoding.
- Model JSON with
Decodable and configure date or key strategies explicitly. - Keep networking out of view bodies and centralize it in a client or service.
- Isolate UI state with
@MainActor. - Support cancellation and distinguish it from failure.
- Use downloads for large files and background sessions for long-running transfers.
- Store secrets in Keychain and use HTTPS.
- Inject sessions so network behavior can be tested deterministically.
Challenges and LimitationsURLSession does not define an API’s business semantics. The developer must understand authentication, pagination, rate limits, versioning, error formats, and idempotency from the service contract. Connectivity may disappear after a request begins, clocks may be inaccurate, and server responses may change. Caching can improve responsiveness but can also display stale data. Offline support requires an explicit local persistence and synchronization strategy rather than simply retrying every request. Finally, asynchronous code reduces thread-management complexity but does not remove the need for data isolation, cancellation design, observability, and security review.
Chapter SummaryURLSession is the central iOS abstraction for HTTP and HTTPS communication. URLRequest expresses the endpoint, method, headers, body, and timeout. async/await makes asynchronous transfers readable without blocking the calling thread. Reliable clients validate HTTP status codes, decode documented response models with Codable, and distinguish transport, server, and decoding failures. Small payloads can use data or upload, while file transfers are better handled by download and, when appropriate, background sessions. UI state should be isolated on the main actor, and tasks should support cancellation. Secure transport, Keychain storage, privacy-aware logging, testable architecture, and explicit retry policies are essential parts of professional networking code.
Key TermsURL: A structured identifier for a network resource. URLRequest: A value describing an HTTP request. URLSession: The framework object that manages network data, upload, and download tasks. HTTP method: A verb such as GET, POST, PUT, PATCH, or DELETE that communicates request intent. Header: Metadata transmitted with a request or response. HTTP status code: A numeric result indicating the broad outcome of an HTTP request. Payload: The body data transmitted in a request or response. URLComponents: A type for safely constructing URLs and query items. Codable: Swift’s protocol composition for encoding and decoding values. JSONDecoder: An object that converts JSON data into Decodable values. async/await: Swift syntax for structured asynchronous programming. Main actor: Swift’s isolation context for main-thread UI-related state. Cancellation: A cooperative request to stop asynchronous work. Timeout: The maximum configured waiting interval for an operation. Upload: Transfer of data from the device to a server. Download: Transfer of data from a server to the device. ATS: App Transport Security rules that encourage secure network connections. Idempotent operation: An operation that has the same intended result when repeated. Dependency injection: Supplying collaborators, such as a URLSession, from outside a type. Review QuestionsShort Answer Questions- What responsibilities are represented by a URLRequest?
- Why should an HTTP status code be checked before decoding a response?
- What is the purpose of the
Accept header? - Why is a downloaded temporary file moved to a destination URL?
- Why should UI state commonly be isolated with
@MainActor?
Long Answer Questions- Explain the complete lifecycle of a JSON GET request using URLRequest, URLSession, validation, and JSONDecoder.
- Compare
data(for:), upload(for:from:), and download(for:), including suitable use cases for each. - Discuss how cancellation, timeouts, and retries affect the reliability of an iOS network client.
- Explain how URLComponents, HTTP headers, Codable models, and dependency injection contribute to maintainable networking code.
- Discuss the security and privacy risks of mobile networking and describe appropriate controls.
Analytical / Application Questions- A search screen sometimes displays results for an earlier query after the user has typed a newer query. Diagnose the likely concurrency problem and propose a solution using task cancellation or request identification.
- An API returns HTTP 200 with a JSON error object instead of the documented book array. Design a validation and decoding strategy that prevents the UI from interpreting the error as an empty book list.
- A user uploads a 200 MB video over an unreliable connection. Recommend a transfer approach, session configuration, error and retry policy, and UI feedback strategy. Justify each choice.
Multiple Choice Questions with AnswersQuestion 1Which type is used to describe an HTTP method, headers, body, and URL together? A. URLRequest B. JSONDecoder C. URLComponents D. URLResponse Correct Answer: A. URLRequest Question 2Which method is most appropriate for receiving a modest JSON response in an async function? A. download(for:) B. data(for:) C. upload(for:from:) D. invalidateAndCancel() Correct Answer: B. data(for:) Question 3What should a client normally do before decoding a response body as a successful result? A. Ignore the status code B. Validate the HTTP response and status code C. Convert the body to a URL D. Retry every response twice Correct Answer: B. Validate the HTTP response and status code Question 4Which Swift type is used to convert JSON data into a Decodable model? A. JSONEncoder B. URLSessionConfiguration C. JSONDecoder D. FileManager Correct Answer: C. JSONDecoder Question 5Why is URLComponents preferred over manually concatenating query strings? A. It automatically authenticates the user B. It percent-encodes query components correctly C. It downloads files in the background D. It guarantees a successful server response Correct Answer: B. It percent-encodes query components correctly Question 6Where should SwiftUI view-model state that directly drives the interface generally be isolated? A. A detached task only B. The global actor for networking C. The main actor D. A URLProtocol subclass Correct Answer: C. The main actor Question 7Which operation is generally safer to retry automatically than a payment-creating POST? A. A GET request B. A password reset request C. A non-idempotent purchase request D. Every request equally Correct Answer: A. A GET request Question 8What is a principal advantage of download(for:) over data(for:) for a large file? A. It guarantees the file is valid B. It writes the result to a temporary file instead of holding all data in memory C. It removes the need for HTTPS D. It automatically stores credentials in Keychain Correct Answer: B. It writes the result to a temporary file instead of holding all data in memory Question 9Where should an authentication token normally be stored on iOS? A. A hard-coded string in the app B. The URL query string C. UserDefaults without protection D. Keychain Correct Answer: D. Keychain Question 10Why is injecting URLSession into an API client useful? A. It prevents all network errors B. It allows deterministic tests with a controlled session or URLProtocol C. It makes every request synchronous D. It bypasses HTTP status codes Correct Answer: B. It allows deterministic tests with a controlled session or URLProtocol
|