textlize pricing account
Production SwiftUI: Scalable Networking Architecture with Async Await
Cover

00:59:18

构建可扩展的 SwiftUI 网络层:以 Async/Await 与协议为核心的分层架构

大多数 iOS 应用都离不开 API 调用和网络请求。随着功能增长,如何管理这些请求、统筹执行顺序,并保持代码的可扩展性可维护性成为关键。本文将通过一个电商演示项目(使用 Dummy JSON API),逐步拆解如何构建一个比“简单 API 请求”更高级、更具复用性的网络架构,并深入探讨 Swift 并发的实际落地方式。

涵盖重点: 泛型网络客户端、自定义错误处理、依赖注入与协议抽象、端点枚举 vs 协议设计、视图模型与 UI 状态的分离、Sendable 与并发安全、可测试的模拟服务。

1. 项目初始化与并发设置

项目基于 Xcode 16.4 创建,这是一个关键细节:未来版本的编译器对并发错误的处理可能略有不同。在构建设置中,有几项与 Swift 并发直接相关:

  • Concurrency Checking:建议设为 Targeted,以获取足够的并发警告信息。
  • Default Actor Isolation:默认会将整个模块标记为 MainActor,但这在实际开发中容易引发大量不必要的编译器错误,需要我们频繁使用 nonisolated 手动退出。因此更推荐关闭该默认行为,让视图和可观察对象按其自身规则正确运行在主线程上。

关闭后,SwiftUI 的 ViewObservable 仍然会自动遵守 MainActor,而我们自定义的网络层则能保持清晰的后台执行环境,减少不必要的编译器干预。

2. 数据模型与响应结构

从 API 返回的 JSON 结构出发,定义符合 Decodable 的实体。演示项目中需要两个核心模型:

  • Product:包含 id、title、description、price、thumbnail 等字段,并遵循 Identifiable 以优化列表性能。
  • ProductsResponse:封装分页信息,包含 products: [Product]totalskiplimit,为后续无限滚动做好准备。

此外,为方便预览与测试,可以为模型提供静态的示例数据。

3. 从基础请求到可复用的 API 客户端

最初,我们可能直接在 ViewModel 中写出如下代码:使用 URLSession.shared.data(from:) 获取数据,再用 JSONDecoder 解码。但这一过程包含着大量重复逻辑:请求构建、状态码校验、错误解析、取消处理等。为了提高复用性,我们逐步提取:

3.1 通用泛型请求方法

将网络请求抽象为一个泛型函数,它接受一个 URLRequest 和返回值类型 T: Decodable,返回 T。内部统一处理:

  • 发起异步数据任务
  • 校验 HTTP 状态码(仅接受 200~299)
  • 解析服务器返回的错误信息
  • 捕获并转换为自定义的 APIError

3.2 结构化错误处理

自定义错误枚举 APIError 涵盖以下场景:

错误类型 说明
invalidResponse 响应无法转换为 HTTPURLResponse
requestFailed(statusCode:message:) 携带状态码与服务器返回的错误消息
networkError 底层 URLSession 错误(如无网络连接)
cancelled 任务被取消(例如视图消失时自动取消),避免向用户展示无意义错误

在 catch 块中,我们特别区分了 URLError.cancelledCancellationError,将它们统一映射为 APIError.cancelled,防止干扰用户交互。

3.3 纯函数的 API Client 结构体

由于这个泛型请求方法不依赖任何外部状态,我们可以把它放入一个值类型(struct)中,命名为 APIClient。它仅包含纯函数,具备以下优势:

  • 内存开销小,自动满足 Sendable 协议,在并发环境中无需额外同步措施。
  • 可在多个 ViewModel 之间安全共享,无需担心竞态条件。
  • 测试友好,可以轻松替换后端实现。

4. 协议驱动:交换真实与模拟服务

为了让 ViewModel 不直接依赖具体的网络实现,也不需要在预览或测试时频繁修改 URL,我们引入服务协议。例如,为产品列表定义:

protocol ProductService {
    func fetchProducts(limit: Int, skip: Int) async throws -> [Product]
}

然后提供两个具体实现:

  • RealProductService:使用 APIClient 访问真实 API。
  • MockProductService:直接返回预设数据或模拟错误场景,极大提升 SwiftUI 预览和单元测试的效率。

ViewModel 仅持有 any ProductService 类型的实例,通过初始化注入即可切换环境。同样的模式也应用于分类列表等其他功能块。

5. 端点协议:告别硬编码 URL

即便使用了 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
}

通过协议扩展,我们为 makeRequestmap 提供了默认实现(使用 JSONDecoder),大多数端点无需重复实现。

对比使用 enum 存放所有 API 案例,协议方案拥有显著优势:

  • 解耦与独立:每个端点文件可归属于对应的功能模块,避免巨型枚举文件引发的合并冲突和难以维护的问题。
  • 易于扩展:新增端点只需新建一个遵循协议的类型,无需修改集中式枚举。
  • 更好的模块化支持:未来拆分 Swift Package 时,端点可以随功能模块独立迁移。

💡 提示:端点的 Response 类型通过 关联类型(associatedtype)与具体接口绑定,使得 APIClient 能自动推断返回值类型,进一步减少调用方的泛型声明负担。

6. 整合端点与客户端:极简服务层

现在,我们将 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
    }
}

经由 EndpointAPIClient 的配合,所有底层细节(HTTP 方法、查询参数拼接、状态码校验、错误解析、JSON 解码)被完全封装,上层仅需关注业务逻辑。这种“薄服务层”模式非常适合大型项目,因为它保证了网络交互的一致性,同时降低了每个功能模块的心智负担。

7. 合理分工:ViewModel 专注 UI 状态

网络层被充分提取后,ViewModel(或某些场景下称为 Store)的职责就变得极为清晰:

  • 持有 @Published 的 UI 状态(如产品列表、加载状态、错误信息)。
  • 通过 Task.task 修饰符触发网络请求。
  • 调用服务层方法并更新状态,所有针对状态的修改自动发生在主线程。

由于我们的 APIClient 和服务层都是纯异步函数,并配合 nonisolated 或显式的后台执行,解码、转换等重任务不会阻塞主线程。一旦异步函数返回,Swift 恢复至 ViewModel 所在的 MainActor 上下文,UI 更新安全且顺畅。

8. 文件组织与模块化建议

随着项目发展,合理的文件结构能避免混乱。推荐按以下层次组织:

🌐 Networking 核心层

存放 APIClientEndpoint 协议、HTTPMethod 枚举及 APIError,可独立为 Swift Package,供整个项目复用。

📦 Feature 模块(如 Product)

包含模型(Product)、端点(ProductEndpoint)、服务协议与实现、ViewModel 以及所有相关视图。每个功能高度内聚。

🧪 模拟数据

Mock 服务与示例数据集中放置,便于 SwiftUI 预览和单元测试引用。

这种结构与 MVVM 的变体相契合:视图层纯粹展示 UI,ViewModel 管理状态并依赖服务层,服务层通过端点与客户端完成网络交互。边界清晰,无论是添加分页、筛选还是搜索功能,都可以在不触碰其他模块的情况下完成。

✅ 架构收益小结:纯函数服务(通常为 struct)+ 协议依赖注入 = 高度可测试;端点协议 + 通用客户端 = 极低的重复代码;显式的并发管理 = 稳定且可预测的线程行为

9. 展望与总结

本文构建的网络架构为后续高级功能(如无限滚动、搜索栏、分类过滤)打下了良好基础。由于服务层与端点层的解耦,新增一个搜索参数只需在对应端点中增加一个查询项,无需改动客户端或 ViewModel 的结构。

核心思想可以浓缩为三点:

  1. 深层模块封装:让底层处理尽可能多的通用逻辑(错误、解码、请求构建),高层代码保持业务纯粹。
  2. 协议隔离变化:通过协议定义服务与端点,利用依赖注入切换真实与模拟实现,加速迭代与测试。
  3. 正确使用并发工具:合理设置 actor isolation,使用 struct 与 Sendable 确保安全,避免在主线程执行繁重任务。

当你面对一个快速膨胀的 iOS 项目时,这样的架构能提供稳固的基石,让团队专注于功能创新而非修复混乱的网络层。


注:本文演示项目基于 Dummy JSON API,完整代码可参阅描述中的项目链接。文中所有 Swift 并发设置基于 Xcode 16.4 / Swift 6 环境。

© 2025 textlize.com. all rights reserved. terms of services privacy policy