00:59:18
大多数 iOS 应用都离不开 API 调用和网络请求。随着功能增长,如何管理这些请求、统筹执行顺序,并保持代码的可扩展性与可维护性成为关键。本文将通过一个电商演示项目(使用 Dummy JSON API),逐步拆解如何构建一个比“简单 API 请求”更高级、更具复用性的网络架构,并深入探讨 Swift 并发的实际落地方式。
涵盖重点: 泛型网络客户端、自定义错误处理、依赖注入与协议抽象、端点枚举 vs 协议设计、视图模型与 UI 状态的分离、Sendable 与并发安全、可测试的模拟服务。
项目基于 Xcode 16.4 创建,这是一个关键细节:未来版本的编译器对并发错误的处理可能略有不同。在构建设置中,有几项与 Swift 并发直接相关:
nonisolated 手动退出。因此更推荐关闭该默认行为,让视图和可观察对象按其自身规则正确运行在主线程上。关闭后,SwiftUI 的 View 与 Observable 仍然会自动遵守 MainActor,而我们自定义的网络层则能保持清晰的后台执行环境,减少不必要的编译器干预。
从 API 返回的 JSON 结构出发,定义符合 Decodable 的实体。演示项目中需要两个核心模型:
Identifiable 以优化列表性能。products: [Product]、total、skip、limit,为后续无限滚动做好准备。此外,为方便预览与测试,可以为模型提供静态的示例数据。
最初,我们可能直接在 ViewModel 中写出如下代码:使用 URLSession.shared.data(from:) 获取数据,再用 JSONDecoder 解码。但这一过程包含着大量重复逻辑:请求构建、状态码校验、错误解析、取消处理等。为了提高复用性,我们逐步提取:
将网络请求抽象为一个泛型函数,它接受一个 URLRequest 和返回值类型 T: Decodable,返回 T。内部统一处理:
APIError自定义错误枚举 APIError 涵盖以下场景:
| 错误类型 | 说明 |
|---|---|
| invalidResponse | 响应无法转换为 HTTPURLResponse |
| requestFailed(statusCode:message:) | 携带状态码与服务器返回的错误消息 |
| networkError | 底层 URLSession 错误(如无网络连接) |
| cancelled | 任务被取消(例如视图消失时自动取消),避免向用户展示无意义错误 |
在 catch 块中,我们特别区分了 URLError.cancelled 与 CancellationError,将它们统一映射为 APIError.cancelled,防止干扰用户交互。
由于这个泛型请求方法不依赖任何外部状态,我们可以把它放入一个值类型(struct)中,命名为 APIClient。它仅包含纯函数,具备以下优势:
Sendable 协议,在并发环境中无需额外同步措施。为了让 ViewModel 不直接依赖具体的网络实现,也不需要在预览或测试时频繁修改 URL,我们引入服务协议。例如,为产品列表定义:
protocol ProductService {
func fetchProducts(limit: Int, skip: Int) async throws -> [Product]
}
然后提供两个具体实现:
ViewModel 仅持有 any ProductService 类型的实例,通过初始化注入即可切换环境。同样的模式也应用于分类列表等其他功能块。
即便使用了 APIClient,请求的构造仍然散落在各处。我们进一步抽象出 Endpoint 协议,将每条 API 的描述集中管理:
protocol Endpoint {
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
var queryItems: [URLQueryItem] { get }
func makeRequest(baseURL: URL) throws -> URLRequest
func map(data: Data) throws -> Response
}
通过协议扩展,我们为 makeRequest 和 map 提供了默认实现(使用 JSONDecoder),大多数端点无需重复实现。
对比使用 enum 存放所有 API 案例,协议方案拥有显著优势:
💡 提示:端点的 Response 类型通过 关联类型(associatedtype)与具体接口绑定,使得 APIClient 能自动推断返回值类型,进一步减少调用方的泛型声明负担。
现在,我们将 APIClient 修改为直接接受一个 Endpoint 参数,由端点内部生成请求并执行解码。服务实现变得异常简洁:
struct RealProductService: ProductService {
let client: APIClient
let baseURL: URL
func fetchProducts(limit: Int, skip: Int) async throws -> [Product] {
let endpoint = ProductEndpoint(limit: limit, skip: skip)
let response: ProductsResponse = try await client.request(endpoint, baseURL: baseURL)
return response.products
}
}
经由 Endpoint 与 APIClient 的配合,所有底层细节(HTTP 方法、查询参数拼接、状态码校验、错误解析、JSON 解码)被完全封装,上层仅需关注业务逻辑。这种“薄服务层”模式非常适合大型项目,因为它保证了网络交互的一致性,同时降低了每个功能模块的心智负担。
网络层被充分提取后,ViewModel(或某些场景下称为 Store)的职责就变得极为清晰:
@Published 的 UI 状态(如产品列表、加载状态、错误信息)。Task 或 .task 修饰符触发网络请求。由于我们的 APIClient 和服务层都是纯异步函数,并配合 nonisolated 或显式的后台执行,解码、转换等重任务不会阻塞主线程。一旦异步函数返回,Swift 恢复至 ViewModel 所在的 MainActor 上下文,UI 更新安全且顺畅。
随着项目发展,合理的文件结构能避免混乱。推荐按以下层次组织:
存放 APIClient、Endpoint 协议、HTTPMethod 枚举及 APIError,可独立为 Swift Package,供整个项目复用。
包含模型(Product)、端点(ProductEndpoint)、服务协议与实现、ViewModel 以及所有相关视图。每个功能高度内聚。
Mock 服务与示例数据集中放置,便于 SwiftUI 预览和单元测试引用。
这种结构与 MVVM 的变体相契合:视图层纯粹展示 UI,ViewModel 管理状态并依赖服务层,服务层通过端点与客户端完成网络交互。边界清晰,无论是添加分页、筛选还是搜索功能,都可以在不触碰其他模块的情况下完成。
✅ 架构收益小结:纯函数服务(通常为 struct)+ 协议依赖注入 = 高度可测试;端点协议 + 通用客户端 = 极低的重复代码;显式的并发管理 = 稳定且可预测的线程行为。
本文构建的网络架构为后续高级功能(如无限滚动、搜索栏、分类过滤)打下了良好基础。由于服务层与端点层的解耦,新增一个搜索参数只需在对应端点中增加一个查询项,无需改动客户端或 ViewModel 的结构。
核心思想可以浓缩为三点:
当你面对一个快速膨胀的 iOS 项目时,这样的架构能提供稳固的基石,让团队专注于功能创新而非修复混乱的网络层。
注:本文演示项目基于 Dummy JSON API,完整代码可参阅描述中的项目链接。文中所有 Swift 并发设置基于 Xcode 16.4 / Swift 6 环境。