ios-networking

iOS networking expert skill covering URLSession with async/await, type-safe generic API clients, Codable JSON encoding/decoding, error handling with retry and exponential backoff, OAuth2 token management, WebSocket connections, caching strategies (URLCache/NSCache), network monitoring (NWPathMonitor), multipart uploads, certificate pinning, and GraphQL with Apollo. Use this skill whenever the user builds networking code, API clients, handles JSON, implements authentication flows, or works with remote data. Triggers on: URLSession, networking, API client, REST, HTTP, JSON, Codable, endpoint, fetch data, download, upload, WebSocket, cache, network monitor, reachability, multipart, GraphQL, Apollo, bearer token, refresh token, retry, backoff, certificate pinning, URL, request, response, async networking.

iOS Networking Skill

Core Rules

  1. Always use async/await (NOT completion handlers) for new networking code. Completion handlers are legacy — Swift Concurrency is the standard since iOS 15.
  2. Build a generic APIClient with an Endpoint protocol for type-safe requests. Never scatter raw URLSession calls throughout the codebase.
  3. Use actor-based TokenManager for thread-safe OAuth2 token refresh with deduplication. Never allow multiple simultaneous refresh requests.
  4. Handle ALL HTTP status codes properly:
    • 401 → refresh token, retry original request
    • 429 → respect Retry-After header, exponential backoff
    • 5xx → retry with exponential backoff + jitter
    • 4xx (other) → client error, do not retry
  5. Use URLProtocol mocking for tests (NOT mocking URLSession itself). URLProtocol intercepts at the transport layer and tests real serialization paths.
  6. Reuse URLSession instances — creating a session per request prevents HTTP/2 connection multiplexing and wastes memory.
  7. Use URLCache with .useProtocolCachePolicy as the default cache policy. Configure cache size explicitly for production apps.
  8. NWPathMonitor for connectivity awareness, NOT pre-flight checks. Never gate a request on reachability — just make the request and handle the error.
  9. Codable with .convertFromSnakeCase and custom date strategies. Avoid manual CodingKeys when snake_case conversion handles it.
  10. Keep ATS enabled. Use domain-specific exceptions in Info.plist only when absolutely necessary. Never disable ATS globally.

Decision Guide

TaskSolutionReference
Simple GET/POSTURLSession.shared.data(for:)urlsession.md
Multiple endpointsGeneric APIClient + Endpoint protocolapi-client.md
Auth with token refreshActor-based TokenManagererror-retry.md
Real-time dataURLSessionWebSocketTaskadvanced.md
Large file downloadURLSession download taskurlsession.md
Background uploadBackground URLSession configurationurlsession.md
Offline supportURLCache + .returnCacheDataElseLoadadvanced.md
Network statusNWPathMonitoradvanced.md
File uploadMultipartFormData builderadvanced.md
Dynamic JSONJSONValue enumapi-client.md
Certificate pinningURLSessionDelegateadvanced.md
GraphQLApollo iOS 2.0advanced.md

Architecture Patterns

Minimal URLSession Call (Quick Reference)

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
        throw NetworkError.httpError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)
    }

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    decoder.dateDecodingStrategy = .iso8601
    return try decoder.decode(User.self, from: data)
}

Production API Client (Quick Reference)

// 1. Define endpoint
struct GetUserEndpoint: Endpoint {
    typealias Response = User
    let userId: Int
    var path: String { "/users/\(userId)" }
    var method: HTTPMethod { .get }
}

// 2. Call through client
let user = try await apiClient.send(GetUserEndpoint(userId: 42))

Authenticated Request Flow

Request → AuthInterceptor (attach token)
       → URLSession.data(for:)
       → 401? → TokenManager.forceRefresh()
              → Retry original request with new token
       → Decode response

Common Mistakes to Avoid

MistakeWhy It's WrongFix
URLSession() per requestKills HTTP/2 multiplexing, leaks memoryReuse a shared or injected session
Checking reachability before requestRace condition, wastes timeJust make the request, handle errors
NSAllowsArbitraryLoads = trueDisables all ATS securityUse domain-specific exceptions
Decoding on main threadBlocks UI for large payloadsURLSession already decodes off-main
Force-unwrapping URLCrashes on malformed stringsUse guard + throw pattern
Ignoring HTTP status codes404/500 treated as successAlways validate response status
Mocking URLSession directlyFragile, doesn't test serializationUse URLProtocol subclass
JSONSerialization for Codable typesVerbose, error-proneUse JSONDecoder/JSONEncoder
Retry without backoffServer overload, ban riskExponential backoff + jitter
Token refresh without dedupMultiple simultaneous refreshesActor with stored Task

File Upload Decision Tree

Need to upload?
├── Small file (<5MB) → URLSession upload task with Data
├── Large file (>5MB) → URLSession upload task with file URL
├── Multiple files → MultipartFormData builder
├── Background upload → Background URLSession config
└── Progress tracking → URLSessionTaskDelegate (async delegate)

HTTP Method Semantics

MethodIdempotentBodyUse Case
GETYesNoFetch resource
POSTNoYesCreate resource
PUTYesYesReplace resource
PATCHNoYesPartial update
DELETEYesOptionalRemove resource
HEADYesNoCheck existence

Testing Strategy

  1. Unit tests: URLProtocol mock → test request building, response parsing, error handling
  2. Integration tests: Staged/sandbox API → test real network stack
  3. Snapshot tests: Capture request/response pairs for regression
// URLProtocol mock setup
final class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override class func canInit(with request: URLRequest) -> Bool { true }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }

    override func startLoading() {
        guard let handler = Self.requestHandler else {
            client?.urlProtocolDidFinishLoading(self)
            return
        }
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {}
}

// Usage in tests
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
let client = APIClient(session: session)

MockURLProtocol.requestHandler = { request in
    let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
    let data = try JSONEncoder().encode(User(id: 1, name: "Test"))
    return (response, data)
}

let user = try await client.send(GetUserEndpoint(userId: 1))
XCTAssertEqual(user.name, "Test")

Performance Checklist

  • Reuse URLSession instances (one per configuration type)
  • Configure httpMaximumConnectionsPerHost (default 6, increase for API-heavy apps)
  • Enable HTTP/2 (default in URLSession, verify server supports it)
  • Set appropriate timeoutIntervalForRequest (30s default, reduce for user-facing)
  • Use waitsForConnectivity = true for non-urgent requests
  • Configure URLCache size (memoryCapacity + diskCapacity)
  • Use AsyncBytes for streaming instead of buffering large responses
  • Cancel tasks when views disappear (use .task modifier in SwiftUI)

Minimum Deployment Targets

FeatureMinimum iOS
URLSession async/await15.0
URLSession.data(for:)15.0
AsyncBytes15.0
URLSessionWebSocketTask13.0
NWPathMonitor12.0
Background URLSession7.0
Codable11.0
async URLSession delegate15.0

References