iOS Networking Skill
Core Rules
- Always use async/await (NOT completion handlers) for new networking code. Completion handlers are legacy — Swift Concurrency is the standard since iOS 15.
- Build a generic APIClient with an Endpoint protocol for type-safe requests. Never scatter raw URLSession calls throughout the codebase.
- Use actor-based TokenManager for thread-safe OAuth2 token refresh with deduplication. Never allow multiple simultaneous refresh requests.
- 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
- Use URLProtocol mocking for tests (NOT mocking URLSession itself). URLProtocol intercepts at the transport layer and tests real serialization paths.
- Reuse URLSession instances — creating a session per request prevents HTTP/2 connection multiplexing and wastes memory.
- Use URLCache with
.useProtocolCachePolicy as the default cache policy. Configure cache size explicitly for production apps.
- NWPathMonitor for connectivity awareness, NOT pre-flight checks. Never gate a request on reachability — just make the request and handle the error.
- Codable with
.convertFromSnakeCase and custom date strategies. Avoid manual CodingKeys when snake_case conversion handles it.
- Keep ATS enabled. Use domain-specific exceptions in Info.plist only when absolutely necessary. Never disable ATS globally.
Decision Guide
| Task |
Solution |
Reference |
| Simple GET/POST |
URLSession.shared.data(for:) |
urlsession.md |
| Multiple endpoints |
Generic APIClient + Endpoint protocol |
api-client.md |
| Auth with token refresh |
Actor-based TokenManager |
error-retry.md |
| Real-time data |
URLSessionWebSocketTask |
advanced.md |
| Large file download |
URLSession download task |
urlsession.md |
| Background upload |
Background URLSession configuration |
urlsession.md |
| Offline support |
URLCache + .returnCacheDataElseLoad |
advanced.md |
| Network status |
NWPathMonitor |
advanced.md |
| File upload |
MultipartFormData builder |
advanced.md |
| Dynamic JSON |
JSONValue enum |
api-client.md |
| Certificate pinning |
URLSessionDelegate |
advanced.md |
| GraphQL |
Apollo iOS 2.0 |
advanced.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
| Mistake |
Why It's Wrong |
Fix |
URLSession() per request |
Kills HTTP/2 multiplexing, leaks memory |
Reuse a shared or injected session |
| Checking reachability before request |
Race condition, wastes time |
Just make the request, handle errors |
NSAllowsArbitraryLoads = true |
Disables all ATS security |
Use domain-specific exceptions |
| Decoding on main thread |
Blocks UI for large payloads |
URLSession already decodes off-main |
| Force-unwrapping URL |
Crashes on malformed strings |
Use guard + throw pattern |
| Ignoring HTTP status codes |
404/500 treated as success |
Always validate response status |
| Mocking URLSession directly |
Fragile, doesn't test serialization |
Use URLProtocol subclass |
JSONSerialization for Codable types |
Verbose, error-prone |
Use JSONDecoder/JSONEncoder |
| Retry without backoff |
Server overload, ban risk |
Exponential backoff + jitter |
| Token refresh without dedup |
Multiple simultaneous refreshes |
Actor 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
| Method |
Idempotent |
Body |
Use Case |
| GET |
Yes |
No |
Fetch resource |
| POST |
No |
Yes |
Create resource |
| PUT |
Yes |
Yes |
Replace resource |
| PATCH |
No |
Yes |
Partial update |
| DELETE |
Yes |
Optional |
Remove resource |
| HEAD |
Yes |
No |
Check existence |
Testing Strategy
- Unit tests: URLProtocol mock → test request building, response parsing, error handling
- Integration tests: Staged/sandbox API → test real network stack
- 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
Minimum Deployment Targets
| Feature |
Minimum iOS |
| URLSession async/await |
15.0 |
| URLSession.data(for:) |
15.0 |
| AsyncBytes |
15.0 |
| URLSessionWebSocketTask |
13.0 |
| NWPathMonitor |
12.0 |
| Background URLSession |
7.0 |
| Codable |
11.0 |
| async URLSession delegate |
15.0 |
References
- URLSession Fundamentals — configurations, async tasks, background sessions, streaming
- API Client Architecture — Endpoint protocol, generic client, interceptors, Codable patterns
- Error Handling & Retry — NetworkError, exponential backoff, token management
- Advanced Topics — WebSocket, caching, NWPathMonitor, multipart, GraphQL
Related Skills
ios-concurrency — async networking
api-security-hardening — network security
ios-architecture — networking layer
GitNexus Index
This skill is indexed by GitNexus for knowledge graph traversal.
Index path: /Users/localuser/.claude/skills/ios-networking/.gitnexus
Last indexed: 2026-05-23
1---2name: ios-networking3description: 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.4---56# iOS Networking Skill78## Core Rules9101. **Always use async/await** (NOT completion handlers) for new networking code. Completion handlers are legacy — Swift Concurrency is the standard since iOS 15.112. **Build a generic APIClient** with an Endpoint protocol for type-safe requests. Never scatter raw URLSession calls throughout the codebase.123. **Use actor-based TokenManager** for thread-safe OAuth2 token refresh with deduplication. Never allow multiple simultaneous refresh requests.134. **Handle ALL HTTP status codes properly**:14 - 401 → refresh token, retry original request15 - 429 → respect Retry-After header, exponential backoff16 - 5xx → retry with exponential backoff + jitter17 - 4xx (other) → client error, do not retry185. **Use URLProtocol mocking for tests** (NOT mocking URLSession itself). URLProtocol intercepts at the transport layer and tests real serialization paths.196. **Reuse URLSession instances** — creating a session per request prevents HTTP/2 connection multiplexing and wastes memory.207. **Use URLCache** with `.useProtocolCachePolicy` as the default cache policy. Configure cache size explicitly for production apps.218. **NWPathMonitor for connectivity awareness**, NOT pre-flight checks. Never gate a request on reachability — just make the request and handle the error.229. **Codable with `.convertFromSnakeCase`** and custom date strategies. Avoid manual CodingKeys when snake_case conversion handles it.2310. **Keep ATS enabled.** Use domain-specific exceptions in Info.plist only when absolutely necessary. Never disable ATS globally.2425## Decision Guide2627| Task | Solution | Reference |28|------|----------|-----------|29| Simple GET/POST | `URLSession.shared.data(for:)` | [urlsession.md](references/urlsession.md) |30| Multiple endpoints | Generic APIClient + Endpoint protocol | [api-client.md](references/api-client.md) |31| Auth with token refresh | Actor-based TokenManager | [error-retry.md](references/error-retry.md) |32| Real-time data | URLSessionWebSocketTask | [advanced.md](references/advanced.md) |33| Large file download | URLSession download task | [urlsession.md](references/urlsession.md) |34| Background upload | Background URLSession configuration | [urlsession.md](references/urlsession.md) |35| Offline support | URLCache + `.returnCacheDataElseLoad` | [advanced.md](references/advanced.md) |36| Network status | NWPathMonitor | [advanced.md](references/advanced.md) |37| File upload | MultipartFormData builder | [advanced.md](references/advanced.md) |38| Dynamic JSON | JSONValue enum | [api-client.md](references/api-client.md) |39| Certificate pinning | URLSessionDelegate | [advanced.md](references/advanced.md) |40| GraphQL | Apollo iOS 2.0 | [advanced.md](references/advanced.md) |4142## Architecture Patterns4344### Minimal URLSession Call (Quick Reference)4546```swift47func fetchUser(id: Int) async throws -> User {48 let url = URL(string: "https://api.example.com/users/\(id)")!49 let (data, response) = try await URLSession.shared.data(from: url)5051 guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {52 throw NetworkError.httpError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)53 }5455 let decoder = JSONDecoder()56 decoder.keyDecodingStrategy = .convertFromSnakeCase57 decoder.dateDecodingStrategy = .iso860158 return try decoder.decode(User.self, from: data)59}60```6162### Production API Client (Quick Reference)6364```swift65// 1. Define endpoint66struct GetUserEndpoint: Endpoint {67 typealias Response = User68 let userId: Int69 var path: String { "/users/\(userId)" }70 var method: HTTPMethod { .get }71}7273// 2. Call through client74let user = try await apiClient.send(GetUserEndpoint(userId: 42))75```7677### Authenticated Request Flow7879```80Request → AuthInterceptor (attach token)81 → URLSession.data(for:)82 → 401? → TokenManager.forceRefresh()83 → Retry original request with new token84 → Decode response85```8687## Common Mistakes to Avoid8889| Mistake | Why It's Wrong | Fix |90|---------|---------------|-----|91| `URLSession()` per request | Kills HTTP/2 multiplexing, leaks memory | Reuse a shared or injected session |92| Checking reachability before request | Race condition, wastes time | Just make the request, handle errors |93| `NSAllowsArbitraryLoads = true` | Disables all ATS security | Use domain-specific exceptions |94| Decoding on main thread | Blocks UI for large payloads | URLSession already decodes off-main |95| Force-unwrapping URL | Crashes on malformed strings | Use guard + throw pattern |96| Ignoring HTTP status codes | 404/500 treated as success | Always validate response status |97| Mocking URLSession directly | Fragile, doesn't test serialization | Use URLProtocol subclass |98| `JSONSerialization` for Codable types | Verbose, error-prone | Use JSONDecoder/JSONEncoder |99| Retry without backoff | Server overload, ban risk | Exponential backoff + jitter |100| Token refresh without dedup | Multiple simultaneous refreshes | Actor with stored Task |101102## File Upload Decision Tree103104```105Need to upload?106├── Small file (<5MB) → URLSession upload task with Data107├── Large file (>5MB) → URLSession upload task with file URL108├── Multiple files → MultipartFormData builder109├── Background upload → Background URLSession config110└── Progress tracking → URLSessionTaskDelegate (async delegate)111```112113## HTTP Method Semantics114115| Method | Idempotent | Body | Use Case |116|--------|-----------|------|----------|117| GET | Yes | No | Fetch resource |118| POST | No | Yes | Create resource |119| PUT | Yes | Yes | Replace resource |120| PATCH | No | Yes | Partial update |121| DELETE | Yes | Optional | Remove resource |122| HEAD | Yes | No | Check existence |123124## Testing Strategy1251261. **Unit tests**: URLProtocol mock → test request building, response parsing, error handling1272. **Integration tests**: Staged/sandbox API → test real network stack1283. **Snapshot tests**: Capture request/response pairs for regression129130```swift131// URLProtocol mock setup132final class MockURLProtocol: URLProtocol {133 static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?134135 override class func canInit(with request: URLRequest) -> Bool { true }136 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }137138 override func startLoading() {139 guard let handler = Self.requestHandler else {140 client?.urlProtocolDidFinishLoading(self)141 return142 }143 do {144 let (response, data) = try handler(request)145 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)146 client?.urlProtocol(self, didLoad: data)147 client?.urlProtocolDidFinishLoading(self)148 } catch {149 client?.urlProtocol(self, didFailWithError: error)150 }151 }152153 override func stopLoading() {}154}155156// Usage in tests157let config = URLSessionConfiguration.ephemeral158config.protocolClasses = [MockURLProtocol.self]159let session = URLSession(configuration: config)160let client = APIClient(session: session)161162MockURLProtocol.requestHandler = { request in163 let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!164 let data = try JSONEncoder().encode(User(id: 1, name: "Test"))165 return (response, data)166}167168let user = try await client.send(GetUserEndpoint(userId: 1))169XCTAssertEqual(user.name, "Test")170```171172## Performance Checklist173174- [ ] Reuse URLSession instances (one per configuration type)175- [ ] Configure `httpMaximumConnectionsPerHost` (default 6, increase for API-heavy apps)176- [ ] Enable HTTP/2 (default in URLSession, verify server supports it)177- [ ] Set appropriate `timeoutIntervalForRequest` (30s default, reduce for user-facing)178- [ ] Use `waitsForConnectivity = true` for non-urgent requests179- [ ] Configure URLCache size (memoryCapacity + diskCapacity)180- [ ] Use `AsyncBytes` for streaming instead of buffering large responses181- [ ] Cancel tasks when views disappear (use `.task` modifier in SwiftUI)182183## Minimum Deployment Targets184185| Feature | Minimum iOS |186|---------|-------------|187| URLSession async/await | 15.0 |188| URLSession.data(for:) | 15.0 |189| AsyncBytes | 15.0 |190| URLSessionWebSocketTask | 13.0 |191| NWPathMonitor | 12.0 |192| Background URLSession | 7.0 |193| Codable | 11.0 |194| async URLSession delegate | 15.0 |195196## References197198- [URLSession Fundamentals](references/urlsession.md) — configurations, async tasks, background sessions, streaming199- [API Client Architecture](references/api-client.md) — Endpoint protocol, generic client, interceptors, Codable patterns200- [Error Handling & Retry](references/error-retry.md) — NetworkError, exponential backoff, token management201- [Advanced Topics](references/advanced.md) — WebSocket, caching, NWPathMonitor, multipart, GraphQL202203## Related Skills204- `ios-concurrency` — async networking205- `api-security-hardening` — network security206- `ios-architecture` — networking layer207208## GitNexus Index209This skill is indexed by GitNexus for knowledge graph traversal.210Index path: /Users/localuser/.claude/skills/ios-networking/.gitnexus211Last indexed: 2026-05-23