SwiftUI 17-使用 Moya + MVVM + SwiftUI 構(gòu)建網(wǎng)絡(luò)請(qǐng)求架構(gòu)的完整實(shí)踐

一、前言

在 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è)試性。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容