一、前言
在 iOS 開發(fā)中,構(gòu)建一個(gè)解耦、清晰、可測(cè)試的網(wǎng)絡(luò)請(qǐng)求體系極其重要。使用 Moya(基于 Alamofire 的網(wǎng)絡(luò)抽象層)配合 MVVM 架構(gòu) 和 SwiftUI,不僅能規(guī)范網(wǎng)絡(luò)層的職責(zé)劃分,還能大幅提升代碼的可讀性和擴(kuò)展性。
本文將介紹如何使用 Moya + MVVM + SwiftUI 搭建一個(gè)清晰、易維護(hù)的網(wǎng)絡(luò)架構(gòu),并通過(guò)「用戶列表」的 Demo 示例,完整講解每一層的實(shí)現(xiàn)與職責(zé)。
二、Moya 簡(jiǎn)介
什么是 Moya?
Moya 是對(duì) Alamofire 的進(jìn)一步封裝,它將接口抽象成 enum 并集中配置,有助于實(shí)現(xiàn):
結(jié)構(gòu)清晰:所有接口通過(guò)枚舉集中管理;
配置統(tǒng)一:請(qǐng)求路徑、方法、參數(shù)、Headers 等集中定義;
易于測(cè)試:通過(guò)
sampleData模擬數(shù)據(jù)響應(yīng);更易擴(kuò)展:支持插件機(jī)制(如日志、緩存等);
三、項(xiàng)目結(jié)構(gòu)設(shè)計(jì)(Moya + MVVM + SwiftUI)
MoyaMVVMSwiftUI/
├── Model/
│ └── User.swift # 數(shù)據(jù)結(jié)構(gòu)
├── Network/
│ ├── APIService.swift # 封裝請(qǐng)求執(zhí)行邏輯
│ └── UserAPI.swift # 定義接口
├── ViewModel/
│ └── UserViewModel.swift # ViewModel
├── View/
│ └── ContentView.swift # UI層
├── MoyaMVVMSwiftUIApp.swift # App 啟動(dòng)入口
四、代碼實(shí)現(xiàn)詳解
1. Model 層:數(shù)據(jù)結(jié)構(gòu)
import Foundation
struct User: Identifiable, Codable {
let id: Int
let name: String
let username: String
let email: String
}
2. Network 層:使用 Moya 封裝請(qǐng)求
UserAPI.swift — 定義接口元數(shù)據(jù)
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()
}
}
作用說(shuō)明:
UserAPI是所有用戶相關(guān)請(qǐng)求的集合,未來(lái)可以擴(kuò)展更多接口,比如.getUserDetail(id)。每個(gè) case 對(duì)應(yīng)一個(gè)獨(dú)立接口。
遵循
TargetType協(xié)議,可以統(tǒng)一定義路徑、方法、參數(shù)、Headers。
APIService.swift — 執(zhí)行請(qǐng)求并解碼數(shù)據(jù)
import Moya
import Foundation
/// 網(wǎng)絡(luò)請(qǐng)求相關(guān)錯(cuò)誤枚舉
enum APIError: Error {
/// HTTP 響應(yīng)狀態(tài)碼不在成功范圍(200...299)時(shí)返回,攜帶具體狀態(tài)碼
case invalidStatusCode(Int)
/// JSON 解析失敗時(shí)返回,攜帶具體的解碼錯(cuò)誤信息
case decodingError(DecodingError)
/// 網(wǎng)絡(luò)請(qǐng)求本身失敗時(shí)返回,比如斷網(wǎng)、超時(shí)等,攜帶底層錯(cuò)誤
case networkError(Error)
/// 其他未知錯(cuò)誤的兜底,攜帶錯(cuò)誤信息
case unknown(Error)
}
class APIService<T: TargetType> {
// 1. 定義了一個(gè) MoyaProvider,負(fù)責(zé)實(shí)際發(fā)起網(wǎng)絡(luò)請(qǐng)求
private let provider: MoyaProvider<T>
// 2. JSON 解碼器,配置了常用的解碼策略
private let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // 下劃線轉(zhuǎn)駝峰
decoder.dateDecodingStrategy = .iso8601 // ISO8601 格式的日期字符串自動(dòng)轉(zhuǎn) Date
return decoder
}()
// 3. 初始化方法,允許傳入 stub 參數(shù),控制是否用模擬數(shù)據(jù)
init(stub: Bool = false) {
if stub {
// 4. 如果是測(cè)試模式,使用立即返回模擬數(shù)據(jù)的 provider
provider = MoyaProvider<T>(stubClosure: MoyaProvider.immediatelyStub)
} else {
// 5. 否則使用默認(rèn)的 provider,正常發(fā)起網(wǎng)絡(luò)請(qǐng)求
provider = MoyaProvider<T>()
}
}
// 6. 發(fā)送請(qǐng)求的異步方法,支持 await 調(diào)用,返回泛型 D,要求遵守 Decodable
func request<D: Decodable>(_ target: T, type: D.Type) async throws -> D {
// 7. 使用 Swift 的 async/await 的橋接,將回調(diào)包裝成異步函數(shù)
try await withCheckedThrowingContinuation { continuation in
// 8. 調(diào)用 MoyaProvider 發(fā)起請(qǐng)求,傳入 target(API 路徑、參數(shù)等)
provider.request(target) { result in
switch result {
case .success(let response):
// 9. 判斷 HTTP 狀態(tài)碼是否是 2xx,非 2xx 就拋錯(cuò)
guard (200...299).contains(response.statusCode) else {
continuation.resume(throwing: APIError.invalidStatusCode(response.statusCode))
return
}
do {
// 10. 用 JSONDecoder 把服務(wù)器返回的 Data 解碼成模型 D
let decoded = try self.decoder.decode(D.self, from: response.data)
// 11. 成功就通過(guò) continuation 把結(jié)果返回給調(diào)用者
continuation.resume(returning: decoded)
} catch let decodingError as DecodingError {
// 12. 解碼出錯(cuò),包裝成 decodingError 拋出
continuation.resume(throwing: APIError.decodingError(decodingError))
} catch {
// 13. 其他錯(cuò)誤用 unknown 包裝拋出
continuation.resume(throwing: APIError.unknown(error))
}
case .failure(let error):
// 14. 請(qǐng)求失敗,網(wǎng)絡(luò)錯(cuò)誤包裝拋出
continuation.resume(throwing: APIError.networkError(error))
}
}
}
}
}
作用說(shuō)明:
| 功能 | 說(shuō)明 |
|---|---|
| 請(qǐng)求發(fā)起 | 調(diào)用 provider.request() 發(fā)起網(wǎng)絡(luò)請(qǐng)求 |
| 響應(yīng)解析 | 使用 JSONDecoder 解碼為 [User] 數(shù)組 |
| 錯(cuò)誤統(tǒng)一處理 | 請(qǐng)求失敗或解析失敗都會(huì)返回 completion(.failure)
|
| 單例模式(可選) | 全局共享 APIService.shared,也可注入 |
是否每個(gè)請(qǐng)求都要寫一個(gè) APIService?
不需要!通常一個(gè)模塊或一個(gè) App 可共用一個(gè)
APIService,你可以添加多個(gè)方法調(diào)用不同 API 枚舉,比如:
fetchUsers()調(diào)用UserAPI
fetchPosts()調(diào)用PostAPI
loginUser()調(diào)用AuthAPI
3. ViewModel 層:業(yè)務(wù)邏輯和狀態(tài)綁定
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
}
}
說(shuō)明:
使用
@Published讓視圖自動(dòng)響應(yīng)數(shù)據(jù)變化;請(qǐng)求狀態(tài)通過(guò)
isLoading管理;錯(cuò)誤信息統(tǒng)一暴露
errorMessage給視圖層處理;解耦 UI 與網(wǎng)絡(luò)層,僅暴露處理后的
users數(shù)據(jù)。
4. View 層:使用 SwiftUI 展示數(shù)據(jù)
// 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("錯(cuò)誤:\(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 啟動(dòng)入口
// MoyaMVVMSwiftUIApp.swift
import SwiftUI
@main
struct MoyaMVVMSwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
五、總結(jié)
通過(guò)結(jié)合 Moya + MVVM + SwiftUI,我們實(shí)現(xiàn)了:
- 網(wǎng)絡(luò)請(qǐng)求邏輯和 UI 徹底解耦
- 明確的職責(zé)劃分(Model-View-ViewModel)
- 更易維護(hù)和測(cè)試的架構(gòu)體系
- 支持模塊化擴(kuò)展和插件注入
這套架構(gòu)非常適合中大型 iOS 項(xiàng)目,特別是在網(wǎng)絡(luò)接口繁多、邏輯復(fù)雜的場(chǎng)景中。你可以繼續(xù)擴(kuò)展更多 API 枚舉 和 Service 方法,并保持良好的代碼結(jié)構(gòu)和測(cè)試性。