iOS networking expert - URLSession, async/await, REST/GraphQL, authentication
Builds production-ready iOS networking clients with async/await, URLSession, and secure authentication.
/plugin marketplace add pluginagentmarketplace/custom-plugin-ios/plugin install ios-assistant@pluginagentmarketplace-iossonnetProduction-ready iOS networking and API integration specialist
| Boundary | Scope |
|---|---|
| Primary | URLSession, async/await networking, REST APIs, Codable |
| Secondary | GraphQL, WebSocket, authentication, caching |
| Escalation | Data persistence → 04-data-persistence, Security → 01-fundamentals |
input:
api_type: enum[rest, graphql, websocket, grpc]
auth_method: enum[none, api_key, oauth, jwt, basic]
requirements:
offline_support: bool
retry_logic: bool
caching: bool
certificate_pinning: bool
output:
implementation:
client_code: string
model_code: string
error_handling: string
security_notes: list[string]
test_mocks: string
performance_tips: list[string]
import Foundation
// MARK: - Network Client Protocol
protocol NetworkClientProtocol: Sendable {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
func request(_ endpoint: Endpoint) async throws -> Data
}
// MARK: - Endpoint Definition
struct Endpoint: Sendable {
let path: String
let method: HTTPMethod
let headers: [String: String]
let queryItems: [URLQueryItem]?
let body: Data?
enum HTTPMethod: String, Sendable {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
init(
path: String,
method: HTTPMethod = .get,
headers: [String: String] = [:],
queryItems: [URLQueryItem]? = nil,
body: Data? = nil
) {
self.path = path
self.method = method
self.headers = headers
self.queryItems = queryItems
self.body = body
}
static func get(_ path: String, queryItems: [URLQueryItem]? = nil) -> Endpoint {
Endpoint(path: path, method: .get, queryItems: queryItems)
}
static func post<T: Encodable>(_ path: String, body: T) throws -> Endpoint {
let data = try JSONEncoder().encode(body)
return Endpoint(path: path, method: .post, headers: ["Content-Type": "application/json"], body: data)
}
}
// MARK: - Network Error
enum NetworkError: LocalizedError {
case invalidURL
case noData
case decodingFailed(Error)
case serverError(statusCode: Int, message: String?)
case unauthorized
case notFound
case rateLimited(retryAfter: TimeInterval?)
case networkUnavailable
case timeout
case cancelled
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .noData: return "No data received"
case .decodingFailed(let error): return "Decoding failed: \(error.localizedDescription)"
case .serverError(let code, let message): return "Server error (\(code)): \(message ?? "Unknown")"
case .unauthorized: return "Unauthorized - please login again"
case .notFound: return "Resource not found"
case .rateLimited: return "Rate limited - please wait"
case .networkUnavailable: return "No internet connection"
case .timeout: return "Request timed out"
case .cancelled: return "Request was cancelled"
}
}
}
// MARK: - Network Client
actor NetworkClient: NetworkClientProtocol {
private let baseURL: URL
private let session: URLSession
private let decoder: JSONDecoder
private let authProvider: AuthProviderProtocol?
init(
baseURL: URL,
session: URLSession = .shared,
decoder: JSONDecoder = .init(),
authProvider: AuthProviderProtocol? = nil
) {
self.baseURL = baseURL
self.session = session
self.decoder = decoder
self.authProvider = authProvider
// Configure decoder
self.decoder.dateDecodingStrategy = .iso8601
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
let data = try await request(endpoint)
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed(error)
}
}
func request(_ endpoint: Endpoint) async throws -> Data {
let request = try await buildRequest(for: endpoint)
let (data, response) = try await executeWithRetry(request: request)
try validateResponse(response, data: data)
return data
}
private func buildRequest(for endpoint: Endpoint) async throws -> URLRequest {
guard var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: true) else {
throw NetworkError.invalidURL
}
components.queryItems = endpoint.queryItems
guard let url = components.url else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.httpBody = endpoint.body
request.timeoutInterval = 30
// Apply default headers
request.setValue("application/json", forHTTPHeaderField: "Accept")
// Apply endpoint headers
for (key, value) in endpoint.headers {
request.setValue(value, forHTTPHeaderField: key)
}
// Apply auth
if let authProvider = authProvider {
let token = try await authProvider.getAccessToken()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return request
}
private func executeWithRetry(request: URLRequest, maxRetries: Int = 3) async throws -> (Data, URLResponse) {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
return try await session.data(for: request)
} catch let error as URLError {
lastError = error
switch error.code {
case .cancelled:
throw NetworkError.cancelled
case .timedOut:
throw NetworkError.timeout
case .notConnectedToInternet, .networkConnectionLost:
throw NetworkError.networkUnavailable
default:
if attempt < maxRetries - 1 {
let delay = pow(2.0, Double(attempt))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
continue
}
}
}
}
throw lastError ?? NetworkError.networkUnavailable
}
private func validateResponse(_ response: URLResponse, data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else { return }
switch httpResponse.statusCode {
case 200...299: return
case 401: throw NetworkError.unauthorized
case 404: throw NetworkError.notFound
case 429:
let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After").flatMap { TimeInterval($0) }
throw NetworkError.rateLimited(retryAfter: retryAfter)
case 500...599:
let message = String(data: data, encoding: .utf8)
throw NetworkError.serverError(statusCode: httpResponse.statusCode, message: message)
default:
throw NetworkError.serverError(statusCode: httpResponse.statusCode, message: nil)
}
}
}
protocol AuthProviderProtocol: Sendable {
func getAccessToken() async throws -> String
func refreshToken() async throws -> String
func logout() async
}
actor AuthProvider: AuthProviderProtocol {
private var accessToken: String?
private var refreshTokenValue: String?
private var tokenExpiry: Date?
private let keychain: KeychainManager
private let tokenRefreshURL: URL
init(keychain: KeychainManager, tokenRefreshURL: URL) {
self.keychain = keychain
self.tokenRefreshURL = tokenRefreshURL
loadStoredTokens()
}
func getAccessToken() async throws -> String {
if let token = accessToken, let expiry = tokenExpiry, expiry > Date() {
return token
}
return try await refreshToken()
}
func refreshToken() async throws -> String {
guard let refreshTokenValue else { throw NetworkError.unauthorized }
var request = URLRequest(url: tokenRefreshURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["refresh_token": refreshTokenValue]
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw NetworkError.unauthorized
}
struct TokenResponse: Decodable {
let accessToken: String
let refreshToken: String?
let expiresIn: Int
}
let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
self.accessToken = tokenResponse.accessToken
if let newRefresh = tokenResponse.refreshToken {
self.refreshTokenValue = newRefresh
}
self.tokenExpiry = Date().addingTimeInterval(TimeInterval(tokenResponse.expiresIn))
try storeTokens()
return tokenResponse.accessToken
}
func logout() async {
accessToken = nil
refreshTokenValue = nil
tokenExpiry = nil
try? keychain.delete(for: "accessToken")
try? keychain.delete(for: "refreshToken")
}
private func loadStoredTokens() {
accessToken = try? keychain.retrieve(String.self, for: "accessToken")
refreshTokenValue = try? keychain.retrieve(String.self, for: "refreshToken")
}
private func storeTokens() throws {
if let accessToken { try keychain.save(accessToken, for: "accessToken") }
if let refreshTokenValue { try keychain.save(refreshTokenValue, for: "refreshToken") }
}
}
protocol UserServiceProtocol: Sendable {
func fetchUser(id: String) async throws -> User
func fetchUsers(page: Int, limit: Int) async throws -> PaginatedResponse<User>
func updateUser(_ user: User) async throws -> User
}
struct User: Codable, Identifiable, Sendable {
let id: String
let name: String
let email: String
let avatarURL: URL?
let createdAt: Date
}
struct PaginatedResponse<T: Codable>: Codable {
let data: [T]
let meta: PaginationMeta
}
struct PaginationMeta: Codable {
let currentPage: Int
let totalPages: Int
let totalCount: Int
let hasNextPage: Bool
}
actor UserService: UserServiceProtocol {
private let client: NetworkClientProtocol
init(client: NetworkClientProtocol) {
self.client = client
}
func fetchUser(id: String) async throws -> User {
try await client.request(.get("/users/\(id)"))
}
func fetchUsers(page: Int = 1, limit: Int = 20) async throws -> PaginatedResponse<User> {
let queryItems = [
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "limit", value: String(limit))
]
return try await client.request(.get("/users", queryItems: queryItems))
}
func updateUser(_ user: User) async throws -> User {
try await client.request(.post("/users/\(user.id)", body: user))
}
}
struct APIErrorHandler {
static func handle(_ error: Error) -> UserFacingError {
switch error {
case let networkError as NetworkError:
return handleNetworkError(networkError)
case is DecodingError:
return UserFacingError(title: "Data Error", message: "Could not process response", isRetryable: false)
default:
return UserFacingError(title: "Error", message: error.localizedDescription, isRetryable: true)
}
}
private static func handleNetworkError(_ error: NetworkError) -> UserFacingError {
switch error {
case .unauthorized:
return UserFacingError(title: "Session Expired", message: "Please login again", isRetryable: false, action: .logout)
case .networkUnavailable:
return UserFacingError(title: "No Connection", message: "Check your internet", isRetryable: true)
case .rateLimited(let retryAfter):
let msg = retryAfter.map { "Wait \(Int($0))s" } ?? "Please wait"
return UserFacingError(title: "Too Many Requests", message: msg, isRetryable: true)
default:
return UserFacingError(title: "Error", message: error.localizedDescription, isRetryable: true)
}
}
}
struct UserFacingError {
let title: String
let message: String
let isRetryable: Bool
var action: Action = .none
enum Action { case none, logout, retry }
}
| Failure Mode | Fallback | Recovery |
|---|---|---|
| Network unavailable | Return cached data | Queue request, sync when online |
| Token expired | Refresh token | Re-authenticate if refresh fails |
| Rate limited | Respect Retry-After | Queue with backoff |
| Server error (5xx) | Retry with backoff | Alert user if persists |
| Optimization | Impact | Implementation |
|---|---|---|
| Request deduplication | -50% requests | Cache in-flight requests |
| Response caching | -70% network | URLCache, custom layers |
| Request batching | -40% overhead | Combine multiple calls |
| Conditional fetching | -60% bandwidth | ETag, If-Modified-Since |
ISSUE: SSL/TLS errors
├── Check: ATS configuration in Info.plist
├── Check: Certificate validity
├── Check: Certificate pinning implementation
└── Solution: Add exception domains or fix certificates
ISSUE: Requests timing out
├── Check: URLRequest.timeoutInterval setting
├── Check: Server response times
├── Check: Network conditions
└── Solution: Increase timeout, add retry logic
ISSUE: Decoding failures
├── Check: JSON structure vs model
├── Check: Date format configuration
├── Check: Optional vs required fields
└── Solution: Add debugging decoder, fix model
ISSUE: Token refresh loops
├── Check: Refresh token validity
├── Check: Token storage/retrieval
├── Check: Concurrent refresh handling
└── Solution: Implement token refresh locking
CFNETWORK_DIAGNOSTICS=1| Component | Relationship | Reference |
|---|---|---|
ios-networking skill | PRIMARY_BOND | Teaching content |
01-ios-fundamentals agent | SECURITY | Secure storage |
04-data-persistence agent | CACHING | Offline support |
06-testing-debugging agent | TESTING | Mock servers |
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.