SwiftUI 17-使用 Moya + MVVM + SwiftUI 构建网络请求架构的完整实践

一、前言

在 iOS 开发中,构建一个解耦、清晰、可测试的网络请求体系极其重要。使用 Moya(基于 Alamofire 的网络抽象层)配合 MVVM 架构 和 SwiftUI,不仅能规范网络层的职责划分,还能大幅提升代码的可读性和扩展性。

本文将介绍如何使用 Moya + MVVM + SwiftUI 搭建一个清晰、易维护的网络架构,并通过「用户列表」的 Demo 示例,完整讲解每一层的实现与职责。

二、Moya 简介

什么是 Moya?

Moya 是对 Alamofire 的进一步封装,它将接口抽象成 enum 并集中配置,有助于实现:

  • 结构清晰:所有接口通过枚举集中管理;

  • 配置统一:请求路径、方法、参数、Headers 等集中定义;

  • 易于测试:通过 sampleData 模拟数据响应;

  • 更易扩展:支持插件机制(如日志、缓存等);

三、项目结构设计(Moya + MVVM + SwiftUI)

MoyaMVVMSwiftUI/
├── Model/
│   └── User.swift               # 数据结构
├── Network/
│   ├── APIService.swift         # 封装请求执行逻辑
│   └── UserAPI.swift            # 定义接口
├── ViewModel/
│   └── UserViewModel.swift      # ViewModel
├── View/
│   └── ContentView.swift        # UI层
├── MoyaMVVMSwiftUIApp.swift     # App 启动入口

四、代码实现详解

1. Model 层:数据结构

import Foundation

struct User: Identifiable, Codable {
    let id: Int
    let name: String
    let username: String
    let email: String
}

2. Network 层:使用 Moya 封装请求

UserAPI.swift — 定义接口元数据

import Moya
import Foundation

enum UserAPI {
    case getUsers
    case getUserDetail(id: Int)
}

extension UserAPI: TargetType {
    var baseURL: URL {
        return URL(string: "https://jsonplaceholder.typicode.com")!
    }

    var path: String {
        switch self {
        case .getUsers:
            return "/users"
        case .getUserDetail(let id):
            return "/users/\(id)"
        }
    }

    var method: Moya.Method {
        return .get
    }

    var task: Task {
        return .requestPlain
    }

    var headers: [String: String]? {
        return ["Content-Type": "application/json"]
    }

    var sampleData: Data {
        return Data()
    }
}

作用说明:

  • UserAPI 是所有用户相关请求的集合,未来可以扩展更多接口,比如 .getUserDetail(id)。

  • 每个 case 对应一个独立接口。

  • 遵循 TargetType 协议,可以统一定义路径、方法、参数、Headers。

APIService.swift — 执行请求并解码数据

import Moya
import Foundation

/// 网络请求相关错误枚举
enum APIError: Error {
    /// HTTP 响应状态码不在成功范围(200...299)时返回,携带具体状态码
    case invalidStatusCode(Int)
    
    /// JSON 解析失败时返回,携带具体的解码错误信息
    case decodingError(DecodingError)
    
    /// 网络请求本身失败时返回,比如断网、超时等,携带底层错误
    case networkError(Error)
    
    /// 其他未知错误的兜底,携带错误信息
    case unknown(Error)
}

class APIService<T: TargetType> {
    // 1. 定义了一个 MoyaProvider,负责实际发起网络请求
    private let provider: MoyaProvider<T>

    // 2. JSON 解码器,配置了常用的解码策略
    private let decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase  // 下划线转驼峰
        decoder.dateDecodingStrategy = .iso8601              // ISO8601 格式的日期字符串自动转 Date
        return decoder
    }()

    // 3. 初始化方法,允许传入 stub 参数,控制是否用模拟数据
    init(stub: Bool = false) {
        if stub {
            // 4. 如果是测试模式,使用立即返回模拟数据的 provider
            provider = MoyaProvider<T>(stubClosure: MoyaProvider.immediatelyStub)
        } else {
            // 5. 否则使用默认的 provider,正常发起网络请求
            provider = MoyaProvider<T>()
        }
    }

    // 6. 发送请求的异步方法,支持 await 调用,返回泛型 D,要求遵守 Decodable
    func request<D: Decodable>(_ target: T, type: D.Type) async throws -> D {
        // 7. 使用 Swift 的 async/await 的桥接,将回调包装成异步函数
        try await withCheckedThrowingContinuation { continuation in
            // 8. 调用 MoyaProvider 发起请求,传入 target(API 路径、参数等)
            provider.request(target) { result in
                switch result {
                case .success(let response):
                    // 9. 判断 HTTP 状态码是否是 2xx,非 2xx 就抛错
                    guard (200...299).contains(response.statusCode) else {
                        continuation.resume(throwing: APIError.invalidStatusCode(response.statusCode))
                        return
                    }
                    do {
                        // 10. 用 JSONDecoder 把服务器返回的 Data 解码成模型 D
                        let decoded = try self.decoder.decode(D.self, from: response.data)
                        // 11. 成功就通过 continuation 把结果返回给调用者
                        continuation.resume(returning: decoded)
                    } catch let decodingError as DecodingError {
                        // 12. 解码出错,包装成 decodingError 抛出
                        continuation.resume(throwing: APIError.decodingError(decodingError))
                    } catch {
                        // 13. 其他错误用 unknown 包装抛出
                        continuation.resume(throwing: APIError.unknown(error))
                    }
                case .failure(let error):
                    // 14. 请求失败,网络错误包装抛出
                    continuation.resume(throwing: APIError.networkError(error))
                }
            }
        }
    }
}

作用说明:

功能 说明
请求发起 调用 provider.request() 发起网络请求
响应解析 使用 JSONDecoder 解码为 [User] 数组
错误统一处理 请求失败或解析失败都会返回 completion(.failure)
单例模式(可选) 全局共享 APIService.shared,也可注入

是否每个请求都要写一个 APIService?

不需要!通常一个模块或一个 App 可共用一个 APIService,你可以添加多个方法调用不同 API 枚举,比如:

  • fetchUsers() 调用 UserAPI
  • fetchPosts() 调用 PostAPI
  • loginUser() 调用 AuthAPI

3. ViewModel 层:业务逻辑和状态绑定

import Foundation

class UserViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let service = APIService<UserAPI>()

    @MainActor
    func fetchUsers() async {
        isLoading = true
        do {
            let result = try await service.request(.getUsers, type: [User].self)
            users = result
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }
}

说明:

  • 使用 @Published 让视图自动响应数据变化;

  • 请求状态通过 isLoading 管理;

  • 错误信息统一暴露 errorMessage 给视图层处理;

  • 解耦 UI 与网络层,仅暴露处理后的 users 数据。

4. View 层:使用 SwiftUI 展示数据

// View/ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = UserViewModel()

    var body: some View {
        NavigationView {
            Group {
                if viewModel.isLoading {
                    ProgressView("加载中...")
                } else if let error = viewModel.errorMessage {
                    Text("错误:\(error)")
                        .foregroundColor(.red)
                } else {
                    List(viewModel.users) { user in
                        VStack(alignment: .leading) {
                            Text(user.name)
                                .font(.headline)
                            Text(user.email)
                                .font(.subheadline)
                        }
                    }
                }
            }
            .navigationTitle("用户列表")
        }
        .task {
            await viewModel.fetchUsers()
        }
    }
}

5. App 启动入口

// MoyaMVVMSwiftUIApp.swift
import SwiftUI

@main
struct MoyaMVVMSwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

五、总结

通过结合 Moya + MVVM + SwiftUI,我们实现了:

  • 网络请求逻辑和 UI 彻底解耦
  • 明确的职责划分(Model-View-ViewModel)
  • 更易维护和测试的架构体系
  • 支持模块化扩展和插件注入

这套架构非常适合中大型 iOS 项目,特别是在网络接口繁多、逻辑复杂的场景中。你可以继续扩展更多 API 枚举 和 Service 方法,并保持良好的代码结构和测试性。

Demo地址 https://github.com/EvanCaiDev/MoyaMVVMSwiftUIApp

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容