一、前言
在 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 方法,并保持良好的代码结构和测试性。