网络层职责
一个健康的网络层应把"请求构造、编解码、错误处理、缓存、鉴权"集中管理,而不是把 URL 字符串撒在 VC 里。目标是:UI 只说"我要用户数据",拿到的就是类型安全的模型。
把网络层拆成:传输层(URLSession 封装)→ 接口层(每个 API 一个方法)→ 业务层(ViewModel 调用)。接口层用协议描述,便于 Mock 替换真实网络。
URLSession 基础
现代写法直接用 async/await 的 data(from:) / data(for:),并在不在主线程的上下文中执行,避免阻塞 UI。
func getUsers() async throws -> [User] {
var req = URLRequest(url: URL(string: "https://api.example.com/users")!)
req.httpMethod = "GET"
req.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw APIError.badStatus
}
return try JSONDecoder().decode([User].self, from: data)
}
生产不滥用 URLSession.shared。用 URLSessionConfiguration 建带超时、缓存策略、自定义代理的专属 session,并按需控制并发数。
Codable 编解码
Codable = Decodable & Encodable,让 JSON 与 Swift 模型双向映射。字段名不一致时用 CodingKeys 或 KeyDecodingStrategy 对齐。
struct User: Codable, Identifiable {
let id: Int
let name: String
let avatarURL: URL
// JSON 是 snake_case:user_name / avatar_url
enum CodingKeys: String, CodingKey {
case id
case name = "user_name"
case avatarURL = "avatar_url"
}
}
// 全局策略:自动把 snake_case 转 camelCase
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
服务端多加字段不会崩(只要模型是 struct 且非全部 required 可选);但删字段或改类型会抛 DecodingError。生产应 catch 解码错误并上报,而不是整页白屏。
请求封装与中间件
把公共逻辑(鉴权头、BaseURL、错误归一化)收进一个 APIClient,业务方法只描述"路径+参数+返回类型"。
protocol APIClient {
func request(_ endpoint: Endpoint) async throws -> T
}
struct Endpoint {
let path: String
let method: String
let query: [URLQueryItem]
}
struct LiveAPIClient: APIClient {
let baseURL = URL(string: "https://api.example.com")!
func request(_ endpoint: Endpoint) async throws -> T {
var comp = URLComponents(url: baseURL.appendingPathComponent(endpoint.path),
resolvingAgainstBaseURL: false)!
comp.queryItems = endpoint.query
var req = URLRequest(url: comp.url!)
req.httpMethod = endpoint.method
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: req)
return try JSONDecoder().decode(T.self, from: data)
}
}
缓存与重试
两个工程重点:用 URLCache 减少重复请求,用指数退避(exponential backoff)应对瞬时故障。
// 1. 配置磁盘缓存
let config = URLSessionConfiguration.default
config.urlCache = URLCache(memoryCapacity: 10_000_000,
diskCapacity: 50_000_000,
directory: nil)
// 2. 指数退避重试
func retry(_ op: () async throws -> T, attempts: Int = 3) async throws -> T {
for i in 0..
.returnCacheDataElseLoad 适合弱实时数据(如配置);强实时(如余额)应禁用缓存或加校验。POST 请求默认不缓存,需手动控制。
Mock 与测试
网络层必须可 Mock:让 ViewModel 依赖 APIClient 协议,测试时注入返回固定数据的 Stub,UI/业务测试完全不触网。
struct MockAPIClient: APIClient {
func request(_ endpoint: Endpoint) async throws -> T {
// 直接返回桩数据,不联网
let json = #"{"id":1,"user_name":"林可","avatar_url":"https://x/y.png"}"#
return try JSONDecoder().decode(T.self, from: Data(json.utf8))
}
}
let vm = UserListViewModel(client: MockAPIClient())
// 单元测试:断言 vm.users 被正确填充,无需真实网络
安全(ATS 与证书)
iOS 默认开启 App Transport Security(ATS),强制 HTTPS。内网自签证书需谨慎配置例外,或用证书绑定(pinning)防中间人。
NSAllowsArbitraryLoads: true 会关闭所有 HTTPS 强制,属于安全反模式,上架审核也会被拒。确需例外时按域名精确配置。
常见陷阱与 FAQ
常被吞掉的错误有:解码失败、非 2xx 没拦截、后台任务被系统回收。统一在网络层 catch 所有错误并打日志/上报,UI 层只拿到归一化的业务错误。
URLSession 现代 API 已足够强大,简单项目无需第三方。Alamofire 在复杂重试链、响应序列化、多表单、鉴权拦截等场景仍有价值,按团队熟悉度取舍。
用 URLSessionDownloadTask 或 bytes(for:) 流式下载到磁盘,配合后台 session 让 App 退到后台也能继续,完成后用文件 URL 而非内存 Data 处理。