Networking, APIs, and ConcurrencyIntroductionMost useful iOS applications communicate with services beyond the device. A weather application retrieves forecasts, a banking application obtains account data, and a collaboration application synchronizes messages with a server. Networking therefore connects the user interface to remote data, but it also introduces uncertainty: servers may be unavailable, connections may be slow, responses may be malformed, and users may cancel an operation before it finishes. This chapter presents the practical foundations required to consume RESTful services in Swift. It focuses on URLSession, HTTP, JSON, Codable, Swift concurrency, authentication, error handling, cancellation, caching, and unreliable connectivity. The examples use modern async/await APIs and are suitable for applications built in Xcode. Learning ObjectivesBy the end of this chapter, you should be able to: - Explain the roles of HTTP, URLs, requests, responses, headers, and status codes.
- Construct REST requests with
URLRequest and execute them with URLSession. - Decode and encode JSON using
Codable. - Design asynchronous networking code using
async/await, tasks, and actors. - Handle authentication, transport failures, server errors, decoding errors, cancellation, and timeouts.
- Apply caching and retry strategies appropriately.
- Design a small networking layer that remains testable and responsive under unreliable connectivity.
1. The Networked Application ModelA networked iOS application normally contains four cooperating layers: - Presentation layer: views and view models display loading, success, empty, and error states.
- Application layer: coordinates user actions and business rules.
- Networking or service layer: constructs requests, sends them, validates responses, and decodes data.
- Remote service: exposes resources through HTTP endpoints and returns representations, commonly JSON.
A request-response exchange is not instantaneous. The application should therefore treat a request as a state transition rather than as an ordinary function that immediately returns a value. A useful conceptual model is: idle → loading → success
↘ failure
↘ cancelledA robust interface does not block the main thread while waiting for a response. It also does not assume that every response is valid merely because the network operation completed. 2. HTTP and REST Fundamentals2.1 URLs and resourcesA URL identifies a resource or an operation endpoint. For example: https://api.example.com/v1/books/42?language=en This URL contains a scheme (https), host (api.example.com), path (/v1/books/42), and query item (language=en). Query items commonly filter, sort, paginate, or otherwise parameterize a request. RESTful services generally model data as resources. Typical HTTP methods are: | Method | Common purpose | Typical property |
|---|
| GET | Retrieve a resource | Safe and usually idempotent | | POST | Create a resource or trigger an action | Usually not idempotent | | PUT | Replace a resource | Idempotent when designed correctly | | PATCH | Partially modify a resource | Depends on implementation | | DELETE | Remove a resource | Commonly idempotent |
A method is safe when it is intended not to change server state. It is idempotent when repeating the same request has the same intended final effect as making it once. Idempotence matters when deciding whether an operation can safely be retried. 2.2 Requests and responsesAn HTTP request contains a method, URL, headers, and, for some methods, a body. A response contains a status code, headers, and an optional body. Headers communicate metadata such as content type, authorization, caching policy, and conditional-request information. Common response classes include: | Range | Meaning | Example |
|---|
| 2xx | Successful request | 200 OK, 201 Created, 204 No Content | | 3xx | Redirection or cache validation | 304 Not Modified | | 4xx | Client-side or request problem | 401 Unauthorized, 404 Not Found, 429 Too Many Requests | | 5xx | Server-side failure | 500 Internal Server Error, 503 Service Unavailable |
URLSession completing without a Swift error does not mean the request succeeded at the application level. A server can return HTTP 401 or 500 while the transport completed normally. The application must inspect the status code.
2.3 JSON representationsJSON represents objects, arrays, strings, numbers, booleans, and null values. A response might be: {
"id": 42,
"title": "Swift in Practice",
"available": true
}JSON is a representation, not a database schema. The client must anticipate missing fields, additional fields, changed values, and incompatible server versions. Decoding should therefore be explicit and errors should be observable rather than silently ignored. 3. URLSession and a Basic RequestURLSession manages HTTP and HTTPS communication. It handles connection establishment, request transmission, response retrieval, redirects, authentication challenges, and some caching behavior. For most application requests, URLSession.shared is sufficient; production applications may create a configured session with explicit timeouts, delegates, or a custom cache.
A minimal asynchronous request is: let url = URL(string: "https://api.example.com/v1/books")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw NetworkError.invalidResponse
}The data(from:) method suspends the task while the request is in progress. It does not freeze the thread. When the operation completes, execution resumes with either data and a response or an error. For greater control, construct a URLRequest: var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 20
let (data, response) = try await URLSession.shared.data(for: request)The Accept header states which response representation the client prefers. For a request containing JSON, the client commonly also sets Content-Type: application/json. 4. Codable and Type-Safe JSONSwift’s Codable protocol combines Encodable and Decodable. It maps Swift types to external representations without requiring manual dictionary casting. struct Book: Codable, Identifiable {
let id: Int
let title: String
let available: Bool
}
let decoder = JSONDecoder()
let book = try decoder.decode(Book.self, from: data)If the server uses a different naming convention, configure the decoder: struct Book: Decodable {
let id: Int
let title: String
let publicationDate: Date
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601With .convertFromSnakeCase, a JSON key such as publication_date can map to publicationDate. Date strategies must match the server’s actual format; .iso8601 is not suitable for every date string. When a key has an unusual name, use CodingKeys: struct Profile: Codable {
let userName: String
let avatarURL: URL?
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case avatarURL = "avatar_url"
}
}A decoding failure is useful diagnostic information. It may indicate a changed API contract, an optionality mistake, a date-format mismatch, or an unexpected response such as an HTML error page. Encoding is similar: struct CreateBookRequest: Encodable {
let title: String
let author: String
}
var request = URLRequest(url: URL(string: "https://api.example.com/v1/books")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(
CreateBookRequest(title: "Networked Swift", author: "A. Student")
)5. A Reusable Networking LayerNetworking logic should not be scattered through view controllers or SwiftUI views. A service layer centralizes request construction, response validation, decoding, authentication, and error translation. enum NetworkError: Error, LocalizedError {
case invalidURL
case invalidResponse
case httpStatus(Int)
case decoding(Error)
case transport(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "The URL was invalid."
case .invalidResponse: return "The server returned an invalid response."
case .httpStatus(let code): return "The server returned HTTP status \(code)."
case .decoding: return "The response could not be read."
case .transport: return "The network connection failed."
}
}
}
struct APIClient {
let session: URLSession
let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func send<T: Decodable>(_ request: URLRequest,
as type: T.Type) async throws -> T {
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard 200..<300 ~= http.statusCode else {
throw NetworkError.httpStatus(http.statusCode)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decoding(error)
}
} catch is CancellationError {
throw CancellationError()
} catch let error as NetworkError {
throw error
} catch {
throw NetworkError.transport(error)
}
}
}A generic method permits one response-validation implementation to decode many response types. In larger systems, error bodies should also be decoded when available so that a server’s structured error message can be shown or logged appropriately. 6. Authentication and SecurityAuthentication establishes who is making a request; authorization determines what that identity may do. Common approaches include: - API keys: a client sends a credential, often in a header. They should not be treated as secret if embedded in a distributed application.
- Bearer tokens: a request includes
Authorization: Bearer <token>. - OAuth 2.0 and OpenID Connect: delegated authorization and identity protocols commonly used with sign-in systems.
- Client certificates or mutual TLS: specialized environments requiring certificate-based authentication.
A bearer-token request might be configured as follows: request.setValue("Bearer \(accessToken)",
forHTTPHeaderField: "Authorization")Never place passwords, long-lived private keys, or production secrets in source code or a public repository. Store tokens in the Keychain rather than ordinary user defaults. Use HTTPS and validate the server’s TLS connection through the system networking stack. Do not disable certificate validation merely to make development requests succeed. Access tokens expire. A professional client needs a policy for a 401 response: refresh the token through an approved authentication flow, retry the original request at most when safe and appropriate, or return the user to sign-in. Refresh logic must prevent multiple simultaneous requests from refreshing the same token independently. 7. Swift Concurrency: Tasks, async/await, and ActorsAn async function may suspend while waiting for an external event. await marks a suspension point and makes asynchronous behavior visible in the source code. A Task creates an asynchronous unit of work: let task = Task {
do {
let books = try await bookService.fetchBooks()
print(books)
} catch {
print(error)
}
}
// Later, if the result is no longer needed:
task.cancel()Cancellation is cooperative. Calling cancel() sets cancellation state; it does not forcibly terminate arbitrary code. System APIs such as URLSession generally respond to cancellation, and application code should check cancellation during lengthy work: try Task.checkCancellation() In a SwiftUI view model, @MainActor ensures that published interface state is accessed on the main actor: @MainActor
final class BooksViewModel: ObservableObject {
@Published private(set) var books: [Book] = []
@Published private(set) var isLoading = false
@Published var message: String?
private let service: BookService
init(service: BookService) {
self.service = service
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
books = try await service.fetchBooks()
} catch is CancellationError {
return
} catch {
message = error.localizedDescription
}
}
}The main actor is not a replacement for good design. Expensive CPU work should not be performed unnecessarily on it. Network suspension itself does not block the main thread, but parsing very large data sets may require careful architecture and measurement. Actors and shared mutable stateAn actor protects mutable state from unsynchronized concurrent access. This is especially useful for an in-memory cache or token manager: actor MemoryCache<Value> {
private var values: [URL: Value] = [:]
func value(for url: URL) -> Value? {
values[url]
}
func insert(_ value: Value, for url: URL) {
values[url] = value
}
}Actor isolation means callers use await to access actor-protected methods from another concurrency domain. Actors reduce data races, but they do not automatically make an algorithm correct; the sequence of operations must still match the application’s requirements. 8. Errors, Timeouts, and Unreliable ConnectivityNetwork failure is not one condition. A useful classification is: - Invalid input: malformed URL or invalid request parameters.
- Transport failure: DNS failure, no route, connection loss, or timeout.
- HTTP failure: a valid response with a 4xx or 5xx status.
- Representation failure: valid transport but invalid or unexpected JSON.
- Application failure: a technically successful response that cannot satisfy a business rule.
Each class suggests a different response. A 401 may require reauthentication; a 404 may indicate absent data; a timeout may justify a retry; a decoding error may require an app update or server correction. Timeouts should be finite. An indefinitely waiting request produces poor user experience and consumes resources. The correct timeout depends on the operation: a search request may use a shorter interval than a large document download. Timeouts are not guarantees that the server stopped processing the request; they only describe the client’s waiting policy. Retries require caution. Retrying a failed GET may be reasonable, but retrying a POST can create duplicate records if the server processed the first request before the connection failed. Retry only operations designed to be idempotent or protected by an idempotency key. Use bounded retries with increasing delays and respect server instructions such as Retry-After. Do not retry every error, especially authentication failures, invalid requests, or most 404 responses. A simple exponential backoff concept is: Dn = min(Dmax, D0 × 2n) Here, Dn is the delay before retry number n, D0 is the initial delay, and Dmax is the maximum delay. The formula assumes a nonnegative integer retry index and is a policy model rather than a guarantee of network behavior. Production systems often add random jitter so that many clients do not retry simultaneously. Connectivity monitoring can inform user messaging, but “connected” does not guarantee that a particular server is reachable. The final authority is the request itself. Applications should support useful offline states, preserve unsent work when appropriate, and explain whether displayed data is current or cached. 9. Caching and Conditional RequestsCaching reduces latency, bandwidth, and server load. There are several levels: - URL loading system cache: may reuse responses according to HTTP cache headers.
- Memory cache: fast but lost when the process ends.
- Disk cache: persists longer but requires storage policy and invalidation.
- Application data store: structured offline data managed by the application.
HTTP caching is governed largely by server headers such as Cache-Control, ETag, and Last-Modified. With an ETag, a client can send If-None-Match. If the resource has not changed, the server can return 304 Not Modified, allowing the client to use its cached representation. Caching must not expose sensitive data to an inappropriate user or retain information longer than policy permits. A banking screen should not blindly cache confidential responses. Applications should also distinguish stale data from fresh data and provide a refresh mechanism. 10. Applied Case: A Course Catalogue ClientBackgroundConsider an illustrative iOS application for a university course catalogue. Users search courses by keyword, open course details, and bookmark courses. The catalogue service is available through a REST API, but students may use the application in lecture rooms with weak connectivity. ProblemA first implementation performs requests directly in view code. It has four problems: the interface freezes while processing results, failed requests display no useful explanation, searches continue after the user changes the query, and repeated detail requests consume bandwidth. AnalysisThe application needs a service abstraction, typed models, cancellation, a cache, and explicit state handling. Search requests are read-only and therefore generally safer to retry than a mutation. However, a search for “data” should be cancelled when the user changes the query to “database”; showing an old result after a newer query completes would be a correctness defect. Approach and SolutionThe service defines a typed endpoint: struct Course: Codable, Identifiable {
let id: String
let code: String
let title: String
let credits: Int
}
struct CourseService {
let client: APIClient
let baseURL: URL
func search(_ term: String) async throws -> [Course] {
var components = URLComponents(url: baseURL.appendingPathComponent("courses"),
resolvingAgainstBaseURL: false)!
components.queryItems = [URLQueryItem(name: "q", value: term)]
guard let url = components.url else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
let result = try await client.send(request, as: CoursePage.self)
return result.items
}
}
struct CoursePage: Decodable {
let items: [Course]
}A view model owns the current search task: @MainActor
final class CourseSearchModel: ObservableObject {
@Published private(set) var courses: [Course] = []
@Published private(set) var isLoading = false
@Published var errorMessage: String?
private let service: CourseService
private var searchTask: Task<Void, Never>?
init(service: CourseService) {
self.service = service
}
func search(for term: String) {
searchTask?.cancel()
let trimmed = term.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
courses = []
return
}
searchTask = Task { [weak self] in
guard let self else { return }
isLoading = true
defer { isLoading = false }
do {
let result = try await service.search(trimmed)
try Task.checkCancellation()
courses = result
errorMessage = nil
} catch is CancellationError {
// A newer query replaced this one.
} catch {
errorMessage = error.localizedDescription
}
}
}
}In a production search field, debouncing may avoid a request for every keystroke. Debouncing delays work until input has remained unchanged for a short policy interval; cancellation ensures obsolete work does not update the interface. Course details can be cached by identifier, while bookmarks should be stored locally and synchronized deliberately rather than treated as disposable response cache data. OutcomeThe interface remains responsive, obsolete searches are cancelled, results are type checked, and failures become visible states. The design also permits dependency injection of a mocked URLSession or service for tests. Learning PointsThe case demonstrates that networking is an application architecture concern, not merely a call to an API. Correctness depends on response validation, concurrency control, cancellation, and appropriate data ownership. 11. Testing and Professional PracticeNetworking code should be tested without depending on a live server. URLProtocol can intercept requests in a custom URLSession configuration and return controlled data, status codes, delays, or errors. Tests should cover: - Successful decoding.
- Malformed JSON.
- 401, 404, 429, and 500 responses.
- Timeouts and loss of connectivity.
- Cancellation before completion.
- Empty results and missing optional values.
- Authentication refresh behavior.
Log request method, host, endpoint category, status code, duration, and a correlation identifier, but avoid logging passwords, tokens, personal information, or complete sensitive response bodies. Define privacy and retention rules for diagnostic data. Accessibility and user experience also matter. Error messages should explain an action the user can take, such as retrying or signing in again. A spinner alone is insufficient for long-running operations; provide cancellation or progress when appropriate. 12. Contemporary Developments and LimitationsSwift concurrency is now the preferred model for new asynchronous Swift code, but applications may still integrate completion handlers, Combine publishers, or third-party clients. Interoperability should be isolated at boundaries rather than allowing multiple concurrency styles to spread unpredictably. Structured concurrency improves reasoning because child tasks have defined lifetimes and cancellation relationships. Unstructured tasks remain useful for user-triggered work, but they must be retained and cancelled when their results are no longer relevant. Network frameworks cannot solve poor API contracts, insecure server configuration, or inconsistent data models. Those problems require coordination between client and server teams. AI-assisted coding can generate request models or boilerplate, but generated networking code must be reviewed for token exposure, incorrect status handling, unsafe retries, and missing cancellation. Security and privacy decisions remain engineering responsibilities. Best Practices Checklist- Use HTTPS and validate authentication securely.
- Keep networking out of view code.
- Validate HTTP status codes explicitly.
- Decode into
Codable models rather than untyped dictionaries where practical. - Represent loading, success, empty, error, and cancelled states.
- Cancel obsolete tasks and check cancellation during lengthy work.
- Protect shared mutable caches or token state with actors or another sound synchronization design.
- Retry selectively, with bounded backoff and attention to idempotence.
- Respect server cache headers and protect sensitive data.
- Test transport, HTTP, decoding, cancellation, and authentication failures.
- Never log credentials or sensitive personal data.
Chapter SummaryRESTful iOS applications exchange representations through HTTP requests and responses. URLRequest expresses method, URL, headers, and body, while URLSession performs asynchronous communication. A completed transport operation must still be checked for valid response type and successful HTTP status. Codable provides type-safe JSON encoding and decoding, with explicit strategies for naming and dates. Swift async/await makes suspension visible and readable; Task represents cancellable work, and actors protect shared mutable state. Reliable applications classify transport, HTTP, decoding, and application errors separately. Authentication requires secure token storage and carefully controlled refresh behavior. Caching, conditional requests, offline states, bounded retries, and informative user feedback help applications function under unreliable connectivity. Finally, a service layer with dependency injection and controlled tests provides a maintainable foundation for professional iOS development. Key TermsURL: A structured address identifying a network resource. HTTP: The protocol used for request-response communication on the web. REST: An architectural style that commonly models server data as resources manipulated through HTTP. JSON: A text-based data interchange format consisting of objects, arrays, values, and null. URLSession: Apple’s framework API for HTTP and related network communication. URLRequest: A Swift value describing an outgoing request. HTTPURLResponse: A response object containing HTTP status and header information. Status code: A numeric HTTP result classification such as 200, 401, or 500. Codable: A Swift protocol composition for encoding and decoding external data. async/await: Swift language features for expressing asynchronous operations and suspension. Task: A unit of asynchronous work that can be cancelled. Cancellation: Cooperative termination of work that is no longer needed. Actor: A Swift reference type that isolates mutable state for concurrency safety. Bearer token: An access credential sent in an HTTP authorization header. Timeout: A limit on how long a client waits for an operation. Idempotence: The property that repeating an operation has the same intended final effect. Caching: Reusing previously obtained data to reduce delay or network use. ETag: A server-provided representation identifier used for conditional requests. Retry: Repeating a failed request according to a controlled policy. Transport error: A failure involving communication, such as timeout or DNS failure. Review QuestionsShort Answer Questions- Why must an application inspect an HTTP status code even when
URLSession reports no Swift error? - What is the purpose of the
Codable protocol? - Explain the difference between
Task.cancel() and forced termination. - Why should access tokens generally be stored in the Keychain?
- What problem does an actor solve in a networking application?
Long Answer Questions- Describe the complete lifecycle of a REST request in an iOS application, from URL construction through decoding and presentation.
- Compare transport failures, HTTP failures, and decoding failures. Give an appropriate response strategy for each.
- Explain how
async/await, Task, and @MainActor work together in a view model. - Discuss the benefits and risks of caching remote data, including HTTP validators and sensitive information.
- Explain why retrying a POST request may be unsafe and describe a responsible retry policy.
Analytical / Application Questions- A course-search screen sends a request for every keystroke. Design a concurrency strategy that prevents obsolete results from replacing newer results.
- An API returns HTTP 401 after an access token expires while three requests are in progress. Propose an authentication-refresh design that avoids three independent refresh operations.
- A mobile user submits a create-order request, loses connectivity, and taps Submit again. Analyze the duplication risk and propose a server-client design using idempotence or an idempotency key.
Multiple Choice Questions with AnswersQuestion 1Which condition must an iOS client check to determine whether an HTTP request was application-level successful? A. That the response contains at least one header B. That the HTTP status code is in the 200–299 range C. That the URL uses a query string D. That the request used GET Correct Answer: B. That the HTTP status code is in the 200–299 range Question 2What is the primary purpose of Codable in a networking layer? A. To encrypt network traffic B. To schedule tasks on the main actor C. To encode and decode structured external data D. To monitor cellular connectivity Correct Answer: C. To encode and decode structured external data Question 3What happens when Task.cancel() is called? A. All application threads are forcibly terminated B. The task is marked for cooperative cancellation C. The server immediately deletes the request D. The task is automatically retried Correct Answer: B. The task is marked for cooperative cancellation Question 4Which HTTP method is normally used to retrieve a resource? A. GET B. POST C. PATCH D. DELETE Correct Answer: A. GET Question 5Why is retrying a POST request potentially dangerous? A. POST responses can never contain JSON B. POST is always blocked by HTTPS C. The operation may create a duplicate resource D. POST requests cannot contain headers Correct Answer: C. The operation may create a duplicate resource Question 6Which Swift feature is designed to isolate mutable state accessed concurrently? A. An actor B. A URLComponent C. A JSONEncoder D. An HTTP status code Correct Answer: A. An actor Question 7Where should a long-lived access token generally be stored on iOS? A. A hard-coded string in the app binary B. The Keychain C. A public URL query item D. A log file Correct Answer: B. The Keychain Question 8What does an ETag support? A. Conditional cache validation B. Password hashing C. Task creation D. JSON pretty printing Correct Answer: A. Conditional cache validation Question 9Which error most directly indicates that the server returned data that did not match the expected Swift model? A. A decoding error B. A successful 204 response C. A valid URL construction D. A cache hit Correct Answer: A. A decoding error Question 10What is the main purpose of bounded exponential backoff with jitter? A. To make every request execute on the main thread B. To avoid uncontrolled repeated requests and synchronized retry bursts C. To convert JSON into XML D. To guarantee that a server is online Correct Answer: B. To avoid uncontrolled repeated requests and synchronized retry bursts
|