要基于 Moya+Alamofire 搭建一套适配 RESTful API、可直接落地生产的 iOS 网络框架,核心需求是实现请求统一管理、全局配置、自动数据解析、统一异常处理、请求解耦 ,同时兼顾易用性、可扩展性、贴合 MVVM 架构 ,让业务层无需关注底层网络细节,一行代码就能发起 RESTful 请求 ------ 这套框架会贴合 RESTful「资源驱动、HTTP 标准」的特性,基于 Swift 5.5+、Moya 15.x+(最新稳定版)实现,包含生产级必备的多环境、请求拦截、加载状态、缓存策略等能力,代码可直接复制复用。
以下方案遵循模块化、协议化、单例化 设计原则,从环境准备→框架核心设计→完整代码封装→初始化配置→业务层使用→进阶扩展逐步实现,贴合 iOS 现代开发思想(Combine/RxSwift、MVVM),同时保留 Moya「枚举管理请求、解耦 URL / 参数」的核心优势,以及 Alamofire 的底层网络能力。
一、前置知识与环境准备
1. 核心依赖关系
Moya 是Alamofire 的上层封装 ,专门解决 RESTful API 的「请求分散、URL 硬编码、参数混乱」问题,核心能力是用枚举集中管理所有 RESTful 请求 ;Alamofire 负责底层 HTTP 请求、网络会话管理,二者搭配是 iOS RESTful 开发的事实标准,无需额外引入其他网络库。
2. Podfile 配置
适配 iOS 13+、Swift 5.5+,引入核心依赖(含 Combine 扩展,贴合现代响应式开发):
ruby
platform :ios, '13.0'
use_frameworks!
target 'YourApp' do
# Moya核心(内置Alamofire依赖,无需单独引入)
pod 'Moya', '~> 15.0'
# Moya的Combine扩展(适配iOS响应式开发,替代闭包)
pod 'Moya/Combine'
# 可选:RxSwift/RxCocoa扩展(若项目用RxSwift而非Combine)
# pod 'Moya/RxSwift'
# JSON解析辅助(可选,简化Codable使用)
pod 'HandyJSON', '~> 5.0'
end
执行 pod install 完成依赖安装,建议使用Xcode 14+ 保证语法兼容。
3. 核心前提:RESTful 规范对齐
框架基于标准 RESTful API 设计,需和服务端对齐以下规则(企业级项目通用):
- URL 规范 :基础路径 + 资源路径(如
https://api.xxx.com/v1/user),版本号放 URL(v1/v2)或请求头; - HTTP 方法:GET(查询)、POST(创建)、PUT(全量更新)、PATCH(局部更新)、DELETE(删除);
- 数据格式 :请求 / 响应均为
JSON,请求头Content-Type: application/json; - 响应格式:服务端返回统一格式(核心!框架解析依赖此格式),示例:
json
{
"code": 200, // 业务状态码(200成功,其他失败)
"message": "success",// 提示信息(失败时为错误描述)
"data": {} // 业务数据(成功时返回,失败时为null/{})
"timestamp": 1735689600 // 可选:时间戳,用于请求校验
}
- 错误码约定 :如
401(Token 过期)、403(无权限)、500(服务端内部错误),框架会统一处理。
二、框架核心设计思路与目录结构
1. 核心设计原则
- 请求解耦:用 Moya 枚举集中管理所有 RESTful 请求,避免 URL / 参数分散在业务层;
- 全局统一:单例管理全局配置(基础 URL、请求头、超时、多环境),支持运行时动态更新;
- 解析自动化 :基于 Swift
Codable封装统一数据解析,业务层直接获取强类型模型,无需手动解析 JSON; - 异常归一化:将 Alamofire/Moya 原生错误、服务端业务错误、网络错误统一为自定义枚举,业务层只需处理一种错误类型;
- 层间隔离:按「配置层→基础层→核心层→工具层」拆分,业务层仅依赖工具层,底层修改不影响上层;
- 贴合 MVVM:基于 Combine/RxSwift 封装请求,返回可观察序列,支持数据绑定和响应式开发。
2. 标准目录结构
在项目中创建Network/RESTful目录,严格按模块拆分,便于维护和扩展,建议直接复用:
plaintext
Network/
└── RESTful/
├── Config/ # 配置层:全局配置、多环境、单例管理
│ ├── NetworkConfig.swift # 全局配置(基础URL、超时、请求头)
│ └── NetworkManager.swift # 核心单例(MoyaProvider管理、环境切换)
├── Base/ # 基础层:通用模型、自定义错误、协议
│ ├── BaseResponse.swift # 统一响应模型(适配服务端返回格式)
│ └── NetworkError.swift # 全局自定义错误枚举(所有错误归一)
├── Core/ # 核心层:Moya请求封装、解析逻辑、拦截器
│ ├── BaseAPI.swift # Moya基础枚举(所有业务API的父类)
│ └── MoyaRequest.swift # 统一请求封装(Combine/RxSwift)
└── Tool/ # 工具层:业务层极简入口(一行代码发起请求)
└── RESTfulRequestTool.swift
业务 API 目录(和 Network 同级,按业务模块拆分请求枚举):
plaintext
API/
├── UserAPI.swift # 用户模块(登录、获取用户信息)
├── OrderAPI.swift # 订单模块(创建订单、查询订单)
└── GoodsAPI.swift # 商品模块(查询商品、加入购物车)
三、框架完整代码封装
所有代码均做了容错处理、内存安全([weak self])、线程切换,贴合生产级要求,关键位置添加详细注释,可直接复制到对应文件中。
3.1 基础层:统一响应模型 + 自定义错误
3.1.1 统一响应模型(BaseResponse.swift)
核心作用 :适配服务端统一的 JSON 返回格式,封装Codable 解析逻辑,业务层只需关注泛型T(实际业务模型),屏蔽通用字段(code/message)。
swift
import Foundation
import Moya
/// RESTful统一响应模型(必须和服务端返回格式严格一致)
/// T:业务数据模型(如User、Order,需遵守Codable)
struct BaseResponse<T: Codable>: Codable {
let code: Int // 业务状态码
let message: String // 提示/错误信息
let data: T? // 业务数据(成功时有值,失败时nil)
let timestamp: TimeInterval? // 可选:服务端时间戳
/// 快速判断是否请求成功(根据业务状态码,默认200为成功)
var isSuccess: Bool {
return code == 200
}
}
/// 无数据响应(用于无需返回业务数据的请求,如退出登录、删除操作)
struct EmptyResponse: Codable {}
3.1.2 全局自定义错误(NetworkError.swift)
核心作用 :将「网络错误、解析错误、服务端业务错误、Token 过期、无网络」等所有异常归一为一个枚举,业务层只需处理NetworkError,屏蔽底层错误复杂性,支持本地化描述和自定义业务错误。
swift
import Foundation
import Moya
import Alamofire
/// RESTful全局自定义错误(覆盖所有可能的错误场景)
enum NetworkError: Error, LocalizedError {
case invalidBaseURL // 无效的基础URL(配置错误)
case moyaError(MoyaError) // Moya/Alamofire原生错误(网络、超时、404/500等)
case serverError(Int, String) // 服务端业务错误(非200状态码,code+message)
case jsonParseError(String) // JSON解析错误(模型和返回数据不匹配)
case emptyResponseData // 服务端返回data为nil(但业务要求有数据)
case tokenExpired // Token过期(自定义业务错误,code=401)
case noNetwork // 无网络(需结合网络检测扩展)
case customError(String) // 本地自定义错误(如参数校验失败)
/// 本地化错误描述(直接给用户看/调试用,支持多语言可扩展)
var errorDescription: String? {
switch self {
case .invalidBaseURL:
return "网络服务地址配置错误"
case .moyaError(let error):
return "网络请求错误:\(error.localizedDescription)"
case .serverError(_, let msg):
return msg.isEmpty ? "服务端业务错误" : msg
case .jsonParseError(let msg):
return "数据解析错误:\(msg)"
case .emptyResponseData:
return "服务端返回空数据"
case .tokenExpired:
return "登录状态已过期,请重新登录"
case .noNetwork:
return "网络连接失败,请检查网络设置"
case .customError(let msg):
return msg
}
}
/// 快速判断是否为Token过期(方便全局处理)
var isTokenExpired: Bool {
return self == .tokenExpired
}
/// 从Moya错误转换为自定义错误(便捷初始化)
static func fromMoyaError(_ error: MoyaError) -> NetworkError {
return .moyaError(error)
}
/// 从服务端响应转换为业务错误(便捷初始化)
static func fromServerCode(_ code: Int, _ msg: String) -> NetworkError {
if code == 401 { return .tokenExpired } // 约定401为Token过期
return .serverError(code, msg)
}
}
3.2 配置层:全局配置 + Moya 单例管理
3.2.1 全局网络配置(NetworkConfig.swift)
核心作用 :管理多环境、基础 URL、全局请求头、超时时间等配置,通过枚举管理多环境(开发 / 测试 / 生产),避免硬编码,支持 Build Configuration 自动切换环境。
swift
import Foundation
import Moya
/// 网络环境枚举(开发/测试/生产,按项目需求扩展)
enum NetworkEnvironment: String {
case dev // 开发环境
case test // 测试环境
case prod // 生产环境
/// 对应环境的基础URL(RESTful核心,所有请求拼接此路径)
var baseURL: String {
switch self {
case .dev:
return "https://dev-api.xxx.com/v1"
case .test:
return "https://test-api.xxx.com/v1"
case .prod:
return "https://api.xxx.com/v1"
}
}
}
/// RESTful全局网络配置(单例+静态配置,App启动时初始化)
final class NetworkConfig {
// 单例(GCD一次性初始化,线程安全)
static let shared = NetworkConfig()
private init() {}
// MARK: - 可配置项(App启动时设置,支持运行时动态更新)
/// 当前网络环境(默认开发环境,可根据Build Configuration自动切换)
var currentEnv: NetworkEnvironment = .dev
/// 全局请求超时时间(默认30秒,单个请求可覆盖)
var timeoutInterval: TimeInterval = 30
/// 全局请求头(如Token、App版本、设备类型,支持运行时动态更新)
var globalHeaders: [String: String] = [:]
/// 是否开启调试模式(DEBUG下打印请求/响应日志,RELEASE关闭)
var isDebug: Bool = true
/// 全局参数(如appId、deviceId,所有请求自动拼接)
var globalParameters: [String: Any] = [:]
// MARK: - 便捷获取当前基础URL
var baseURL: String {
return currentEnv.baseURL
}
// MARK: - 动态更新全局请求头(如Token刷新、用户切换)
func updateGlobalHeaders(_ newHeaders: [String: String]) {
globalHeaders.merge(newHeaders) { _, new in new }
}
// MARK: - 清空全局请求头(如用户退出登录)
func clearGlobalHeaders() {
globalHeaders.removeAll()
}
// MARK: - 动态更新全局参数
func updateGlobalParams(_ newParams: [String: Any]) {
globalParameters.merge(newParams) { _, new in new }
}
}
// MARK: - 可选:根据Build Configuration自动切换环境
extension NetworkConfig {
/// 从Xcode Build Configuration读取环境(需先配置Xcode的Scheme)
func configEnvFromBuildConfig() {
#if DEBUG
currentEnv = .dev
#elseif TEST
currentEnv = .test
#else
currentEnv = .prod
#endif
}
}
3.2.2 Moya 核心单例管理(NetworkManager.swift)
核心作用 :全局唯一管理MoyaProvider实例(Moya 的核心请求器),配置请求拦截器、插件(日志 / 网络活动)、会话管理器 ,支持运行时重新初始化 Provider(如更新请求头后生效),同时封装 Moya 插件(日志、网络状态)。
swift
import Foundation
import Moya
import Alamofire
/// RESTful网络核心单例(管理MoyaProvider、插件、请求拦截)
final class NetworkManager {
// 单例
static let shared = NetworkManager()
private init() {}
// 全局MoyaProvider(泛型为BaseAPI,所有业务API的父类)
private var moyaProvider: MoyaProvider<BaseAPI>!
// MARK: - 初始化MoyaProvider(App启动时调用,核心!)
func setupProvider() {
// 1. 配置Alamofire会话管理器(超时、请求头、信任证书等)
let sessionManager = createSessionManager()
// 2. 配置Moya插件(日志插件、网络活动插件,按需添加)
let plugins = createMoyaPlugins()
// 3. 初始化MoyaProvider(指定会话管理器和插件)
moyaProvider = MoyaProvider<BaseAPI>(
session: sessionManager,
plugins: plugins,
trackInflights: true // 防止重复请求(相同URL+参数的请求合并)
)
}
// MARK: - 对外提供MoyaProvider(业务层无需直接访问)
func getProvider() -> MoyaProvider<BaseAPI> {
if moyaProvider == nil { setupProvider() }
return moyaProvider
}
// MARK: - 重新初始化Provider(动态更新配置后调用,如切换环境、更新Token)
func reloadProvider() {
setupProvider()
}
}
// MARK: - 私有方法:创建Alamofire会话管理器(配置超时、请求头)
extension NetworkManager {
private func createSessionManager() -> Session {
let config = URLSessionConfiguration.default
// 设置全局超时时间
config.timeoutIntervalForRequest = NetworkConfig.shared.timeoutInterval
config.timeoutIntervalForResource = NetworkConfig.shared.timeoutInterval
// 开启HTTP缓存(可选,根据业务需求配置)
config.requestCachePolicy = .useProtocolCachePolicy
// 配置全局请求头(从NetworkConfig读取)
config.httpAdditionalHeaders = NetworkConfig.shared.globalHeaders
// 创建Alamofire会话管理器
return Session(configuration: config)
}
}
// MARK: - 私有方法:创建Moya插件(日志、网络活动,按需扩展)
extension NetworkManager {
private func createMoyaPlugins() -> [PluginType] {
var plugins: [PluginType] = []
// 1. 网络活动插件(控制状态栏网络加载指示器)
plugins.append(NetworkActivityPlugin { state, _ in
switch state {
case .began: UIApplication.shared.isNetworkActivityIndicatorVisible = true
case .ended: UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
})
// 2. 调试日志插件(仅DEBUG模式生效,避免生产环境泄露信息)
if NetworkConfig.shared.isDebug {
plugins.append(CustomLogPlugin())
}
// 可选:添加自定义插件(如请求加密、响应解密、Token刷新)
return plugins
}
}
// MARK: - 自定义日志插件(打印请求/响应详情,替代Moya默认的NetworkLoggerPlugin)
final class CustomLogPlugin: PluginType {
func willSend(_ request: RequestType, target: TargetType) {
// 打印请求信息
print("📡 [RESTful Request] \(target.method.rawValue) | \(target.baseURL.absoluteString + target.path)")
print("📋 [Request Headers] \(request.request?.allHTTPHeaderFields ?? [:])")
if let params = target.parameters {
print("📝 [Request Params] \(params)")
}
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
// 打印成功响应
print("✅ [RESTful Success] \(target.baseURL.absoluteString + target.path) | StatusCode: \(response.statusCode)")
if let json = try? response.mapJSON() {
print("📦 [Response Data] \(json)")
}
case .failure(let error):
// 打印失败信息
print("❌ [RESTful Failure] \(target.baseURL.absoluteString + target.path) | Error: \(error.localizedDescription)")
}
}
}
3.3 核心层:Moya 基础枚举 + 统一请求封装
3.3.1 Moya 基础枚举(BaseAPI.swift)
核心作用 :作为所有业务 API 枚举的父类 ,实现 Moya 的TargetType协议的通用方法(基础 URL、请求头、参数编码等),业务 API 只需实现个性化部分(path、method、parameters),减少重复代码,是 Moya 解耦的核心。
swift
import Foundation
import Moya
/// Moya基础API枚举(所有业务API的父类,实现通用TargetType方法)
/// 业务API只需遵循此协议,实现个性化属性即可
protocol BaseAPI: TargetType {
/// 单个请求的超时时间(nil则使用全局配置)
var requestTimeout: TimeInterval? { get }
/// 是否忽略全局请求头(如登录请求无需Token)
var ignoreGlobalHeaders: Bool { get }
/// 是否忽略全局参数(如部分请求无需拼接appId)
var ignoreGlobalParams: Bool { get }
}
// MARK: - 实现BaseAPI的默认TargetType方法(通用配置,业务层无需重写)
extension BaseAPI {
// 基础URL(从全局配置读取)
var baseURL: URL {
guard let url = URL(string: NetworkConfig.shared.baseURL) else {
fatalError(NetworkError.invalidBaseURL.errorDescription!)
}
return url
}
// 请求方法(默认GET,业务层可重写)
var method: Moya.Method { .get }
// 请求参数(默认空,业务层重写)
var parameters: [String: Any]? { nil }
// 参数编码方式(默认JSON,表单提交可重写为URLEncoding)
var parameterEncoding: ParameterEncoding {
return JSONEncoding.default
}
// 任务类型(默认请求数据,上传/下载可重写)
var task: Task {
// 合并全局参数和单个请求参数
var allParams = NetworkConfig.shared.globalParameters
if let params = parameters, !ignoreGlobalParams {
allParams.merge(params) { _, new in new }
}
return .requestParameters(parameters: allParams, encoding: parameterEncoding)
}
// 请求头(合并全局请求头,单个请求可重写)
var headers: [String: String]? {
ignoreGlobalHeaders ? nil : NetworkConfig.shared.globalHeaders
}
// 单元测试用(默认空,无需实现)
var sampleData: Data { Data() }
// 单个请求超时(默认nil,使用全局)
var requestTimeout: TimeInterval? { nil }
// 是否忽略全局请求头(默认false)
var ignoreGlobalHeaders: Bool { false }
// 是否忽略全局参数(默认false)
var ignoreGlobalParams: Bool { false }
}
// MARK: - 扩展:支持上传/下载任务(按需使用)
extension BaseAPI {
/// 构建文件上传任务(Multipart/form-data,如图片/文件上传)
func uploadTask(with formData: [MultipartFormData]) -> Task {
return .uploadMultipart(formData)
}
/// 构建文件下载任务
func downloadTask(with destination: DownloadDestination) -> Task {
return .downloadDestination(destination)
}
}
3.3.2 统一请求封装(MoyaRequest.swift)
核心作用 :封装 Moya 的原生请求,基于Combine 实现响应式请求,完成统一错误转换、JSON 自动解析、线程切换 (结果切到主线程,方便业务层更新 UI),屏蔽 Moya 的Publisher细节,返回业务层易使用的AnyPublisher。
若项目使用RxSwift ,只需将 Combine 的
Future/AnyPublisher替换为 RxSwift 的Single/Observable,逻辑完全一致。
swift
import Foundation
import Moya
import Combine
/// RESTful请求配置(单个请求自定义配置,覆盖全局)
struct RESTfulRequestConfig {
var cachePolicy: URLRequest.CachePolicy? // 缓存策略(可选)
var retryCount: Int = 0 // 失败重试次数(默认0,不重试)
}
/// Moya请求核心封装(统一处理请求、解析、错误转换)
final class MoyaRequest {
// 单例
static let shared = MoyaRequest()
private init() {}
// 全局MoyaProvider
private var moyaProvider: MoyaProvider<BaseAPI> {
return NetworkManager.shared.getProvider()
}
}
// MARK: - 核心:发起RESTful请求并自动解析为强类型模型
extension MoyaRequest {
/// 执行RESTful请求,自动解析为泛型模型
/// - Parameters:
/// - api: 业务API枚举(遵循BaseAPI)
/// - config: 单个请求配置(可选)
/// - T: 业务数据模型(需遵守Codable)
/// - Returns: AnyPublisher<T, NetworkError>(成功返回业务模型,失败返回自定义错误)
func request<T: Codable>(
_ api: BaseAPI,
config: RESTfulRequestConfig = .init()
) -> AnyPublisher<T, NetworkError> {
return Future<T, NetworkError> { [weak self] promise in
guard let self = self else {
promise(.failure(.customError("MoyaRequest已释放")))
return
}
// 1. 获取Moya请求的Publisher
let requestPublisher = self.moyaProvider.requestPublisher(api)
// 超时处理(覆盖全局/单个请求的超时)
.timeout(api.requestTimeout ?? NetworkConfig.shared.timeoutInterval, scheduler: DispatchQueue.main)
// 重试机制
.retry(config.retryCount)
// 2. 订阅请求结果
_ = requestPublisher.sink(
receiveCompletion: { completion in
if case .failure(let moyaError) = completion {
// Moya原生错误转换为自定义错误
promise(.failure(NetworkError.fromMoyaError(moyaError)))
}
},
receiveValue: { [weak self] response in
// 3. 处理响应数据,统一解析
self?.handleResponse(response, promise: promise)
}
)
}
// 切换到主线程(业务层可直接更新UI,无需手动切换)
.receive(on: DispatchQueue.main)
// 封装为AnyPublisher,屏蔽底层实现
.eraseToAnyPublisher()
}
/// 执行无数据返回的请求(如退出登录、删除),仅返回是否成功
func requestWithoutData(
_ api: BaseAPI,
config: RESTfulRequestConfig = .init()
) -> AnyPublisher<Bool, NetworkError> {
return request<EmptyResponse>(api, config: config)
.map { _ in true } // 解析成功即返回true
.eraseToAnyPublisher()
}
}
// MARK: - 私有方法:统一处理Moya响应,完成JSON解析和错误判断
extension MoyaRequest {
private func handleResponse<T: Codable>(
_ response: Response,
promise: @escaping (Result<T, NetworkError>) -> Void
) {
do {
// 1. 验证HTTP状态码(200~299为成功,否则抛错)
let validResponse = try response.filterSuccessfulStatusCodes()
// 2. 解析为统一响应模型BaseResponse<T>
let baseResponse = try validResponse.decode(BaseResponse<T>.self)
// 3. 判断业务状态码是否成功
if baseResponse.isSuccess {
// 4. 检查业务数据是否为空
guard let data = baseResponse.data else {
promise(.failure(.emptyResponseData))
return
}
// 5. 解析成功,返回业务模型
promise(.success(data))
} else {
// 6. 业务错误,转换为自定义错误
promise(.failure(NetworkError.fromServerCode(baseResponse.code, baseResponse.message)))
}
} catch let moyaError as MoyaError {
// Moya过滤状态码失败(如404/500)
promise(.failure(NetworkError.fromMoyaError(moyaError)))
} catch let decodeError as DecodingError {
// JSON解析错误(模型和数据不匹配)
promise(.failure(.jsonParseError(decodeError.localizedDescription)))
} catch {
// 其他未知错误
promise(.failure(.customError(error.localizedDescription)))
}
}
}
// MARK: - 扩展:Moya Response快速解码为Codable模型
extension Response {
/// 快速将Moya Response解码为遵循Codable的模型
func decode<T: Codable>(_ type: T.Type) throws -> T {
return try JSONDecoder().decode(type, from: self.data)
}
}
3.4 工具层:业务层极简入口(RESTfulRequestTool.swift)
核心作用 :对核心层的MoyaRequest做一层极简封装 ,是业务层唯一需要依赖的文件 ,屏蔽所有底层细节,业务层只需一行代码发起请求,无需创建实例、处理泛型细节,最大限度降低使用成本。
swift
import Foundation
import Moya
import Combine
/// RESTful请求工具类(业务层唯一入口,极简API)
/// 所有业务层请求均通过此类发起,无需接触底层Moya/Alamofire
struct RESTfulRequestTool {
// 私有化构造器,避免实例化
private init() {}
// MARK: - 发起带数据返回的请求(自动解析为强类型模型)
static func request<T: Codable>(
_ api: BaseAPI,
config: RESTfulRequestConfig = .init()
) -> AnyPublisher<T, NetworkError> {
return MoyaRequest.shared.request(api, config: config)
}
// MARK: - 发起无数据返回的请求(仅返回是否成功)
static func requestWithoutData(
_ api: BaseAPI,
config: RESTfulRequestConfig = .init()
) -> AnyPublisher<Bool, NetworkError> {
return MoyaRequest.shared.requestWithoutData(api, config: config)
}
}
// MARK: - Combine扩展:简化订阅,自动存储Cancellable(业务层必备)
/// 给AnyPublisher添加扩展,方便业务层快速订阅,无需手动管理Cancellable
extension AnyPublisher {
/// 快速订阅RESTful请求结果
/// - Parameters:
/// - cancellables: 存储订阅的Cancellable(业务层只需创建Set<AnyCancellable>()即可)
/// - onSuccess: 成功回调(返回强类型模型)
/// - onFailure: 失败回调(返回自定义NetworkError)
func sinkRESTful(
to cancellables: inout Set<AnyCancellable>,
onSuccess: @escaping (Output) -> Void,
onFailure: @escaping (Failure) -> Void
) where Failure == NetworkError {
self.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
onFailure(error)
}
},
receiveValue: { value in
onSuccess(value)
}
)
.store(in: &cancellables)
}
}
四、业务 API 枚举编写(按模块拆分)
基于上述框架,业务层只需按模块编写 API 枚举 (遵循BaseAPI协议),实现path、method、parameters等个性化属性即可,无需关注任何底层网络逻辑 ------ 这是 Moya 解耦的核心优势,所有请求集中管理,便于维护和修改。
以下以用户模块(UserAPI.swift) 为例,包含登录、获取用户信息、更新用户信息、退出登录等典型 RESTful 请求:
swift
import Foundation
import Moya
/// 用户模块API枚举(遵循BaseAPI,实现个性化属性)
/// 所有用户相关请求集中管理,路径清晰,修改方便
enum UserAPI: BaseAPI {
// 登录(POST,忽略全局Token)
case login(account: String, password: String)
// 根据ID获取用户信息(GET,需要用户ID参数)
case fetchUser(id: String)
// 更新用户信息(PUT,传入用户信息参数)
case updateUser(name: String, avatar: String, phone: String)
// 退出登录(POST,无参数,无返回数据)
case logout
// MARK: - 实现BaseAPI的个性化属性
/// 请求路径(拼接在baseURL后,如/baseURL/user/login)
var path: String {
switch self {
case .login: return "/user/login"
case .fetchUser: return "/user/info"
case .updateUser: return "/user/update"
case .logout: return "/user/logout"
}
}
/// HTTP请求方法(重写默认的GET)
var method: Moya.Method {
switch self {
case .login, .updateUser, .logout: return .post
case .fetchUser: return .get
}
}
/// 请求参数(根据不同case拼接参数)
var parameters: [String: Any]? {
switch self {
case .login(let account, let pwd):
return ["account": account, "password": pwd]
case .fetchUser(let id):
return ["userId": id]
case .updateUser(let name, let avatar, let phone):
return ["userName": name, "avatarUrl": avatar, "phone": phone]
case .logout:
return nil
}
}
/// 登录请求忽略全局Token(重写默认的false)
var ignoreGlobalHeaders: Bool {
return self == .login
}
/// 单个请求超时(登录请求超时15秒,重写全局30秒)
var requestTimeout: TimeInterval? {
return self == .login ? 15 : nil
}
}
其他模块(如订单、商品) 按相同方式编写枚举即可,示例:
swift
// OrderAPI.swift
enum OrderAPI: BaseAPI {
case fetchOrderList(page: Int, size: Int) // 获取订单列表(GET,分页参数)
case createOrder(goodsId: [String], totalPrice: Double) // 创建订单(POST)
var path: String {
switch self {
case .fetchOrderList: return "/order/list"
case .createOrder: return "/order/create"
}
}
var method: Moya.Method {
return self == .fetchOrderList ? .get : .post
}
var parameters: [String: Any]? {
switch self {
case .fetchOrderList(let page, let size):
return ["pageNum": page, "pageSize": size]
case .createOrder(let ids, let price):
return ["goodsIds": ids, "totalPrice": price]
}
}
}
五、框架初始化配置(App 启动时执行)
在 App 启动入口(AppDelegate/SceneDelegate,SwiftUI 为App结构体)中完成全局网络配置 和MoyaProvider 初始化 ,这是框架使用的前提,只需配置一次,全局生效。
5.1 UIKit 项目(AppDelegate)
swift
import UIKit
import Combine
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// 全局Cancellable(存储全局订阅,如Token过期监听)
private var cancellables = Set<AnyCancellable>()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. 初始化网络框架(核心!必须先执行)
setupNetworkFramework()
// 2. 全局处理Token过期(如跳转到登录页)
setupGlobalTokenExpiredHandler()
return true
}
// MARK: - 初始化网络框架:配置环境、请求头、初始化MoyaProvider
private func setupNetworkFramework() {
let netConfig = NetworkConfig.shared
// 1. 自动从Build Configuration切换环境(推荐)
netConfig.configEnvFromBuildConfig()
// 手动设置环境(测试用):netConfig.currentEnv = .dev
// 2. 配置全局超时
netConfig.timeoutInterval = 20
// 3. 配置全局请求头(App版本、设备类型、OS版本等)
netConfig.globalHeaders = [
"Content-Type": "application/json",
"App-Version": Bundle.main.appVersion ?? "1.0.0",
"Device-Type": "iOS",
"OS-Version": UIDevice.current.systemVersion,
"Token": UserDefaults.standard.string(forKey: "kUserToken") ?? "" // 本地缓存的Token
]
// 4. 配置全局参数(所有请求自动拼接)
netConfig.globalParameters = [
"appId": "ios_123456",
"deviceId": UIDevice.current.identifierForVendor?.uuidString ?? ""
]
// 5. 开启/关闭调试模式
netConfig.isDebug = true
// 6. 初始化MoyaProvider(核心!)
NetworkManager.shared.setupProvider()
}
// MARK: - 全局处理Token过期(示例:发送通知,跳转到登录页)
private func setupGlobalTokenExpiredHandler() {
NotificationCenter.default.publisher(for: NSNotification.Name("kRESTfulTokenExpired"))
.sink { [weak self] _ in
guard let self = self else { return }
// 1. 清除本地Token
UserDefaults.standard.removeObject(forKey: "kUserToken")
// 2. 清空全局请求头的Token并重新初始化Provider
NetworkConfig.shared.clearGlobalHeaders()
NetworkManager.shared.reloadProvider()
// 3. 跳转到登录页(按项目实际UI架构修改)
self.gotoLoginPage()
}
.store(in: &cancellables)
}
// 跳转到登录页(UIKit示例)
private func gotoLoginPage() {
let loginVC = LoginViewController()
let nav = UINavigationController(rootViewController: loginVC)
window?.rootViewController = nav
window?.makeKeyAndVisible()
}
}
// MARK: - 可选:Bundle扩展(获取App版本号和Build号)
extension Bundle {
var appVersion: String? {
return infoDictionary?["CFBundleShortVersionString"] as? String
}
var appBuild: String? {
return infoDictionary?["CFBundleVersion"] as? String
}
}
5.2 SwiftUI 项目(App 结构体)
swift
import SwiftUI
import Combine
@main
struct YourApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var cancellables = Set<AnyCancellable>()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
// 初始化网络框架
setupNetworkFramework()
// 全局Token过期处理
setupGlobalTokenExpiredHandler()
}
}
}
private func setupNetworkFramework() {
let netConfig = NetworkConfig.shared
netConfig.configEnvFromBuildConfig()
netConfig.timeoutInterval = 20
netConfig.globalHeaders = [
"Content-Type": "application/json",
"App-Version": Bundle.main.appVersion ?? "1.0.0",
"Token": UserDefaults.standard.string(forKey: "kUserToken") ?? ""
]
NetworkManager.shared.setupProvider()
}
private func setupGlobalTokenExpiredHandler() {
NotificationCenter.default.publisher(for: NSNotification.Name("kRESTfulTokenExpired"))
.sink { _ in
UserDefaults.standard.removeObject(forKey: "kUserToken")
NetworkConfig.shared.clearGlobalHeaders()
NetworkManager.shared.reloadProvider()
// SwiftUI跳转到登录页(通过NavigationStack/Environment实现)
}
.store(in: &cancellables)
}
}
extension Bundle {
var appVersion: String? {
infoDictionary?["CFBundleShortVersionString"] as? String
}
}
六、业务层使用示例(MVVM 架构,核心:一行代码发起请求)
框架贴合MVVM+Combine 的现代 iOS 开发思想,View 层 仅负责 UI 展示和事件绑定,ViewModel 层 发起网络请求、处理业务逻辑,通过@Published将数据 / 状态暴露给 View 层,实现视图与业务解耦。
以下以用户模块为例,完成「登录、获取用户信息」的完整业务层使用流程。
6.1 ViewModel 层(核心业务逻辑)
封装网络请求、业务逻辑,通过@Published发布数据、加载状态、错误信息,View 层只需绑定即可,无需处理任何网络细节。
swift
import Foundation
import Combine
// 基础ViewModel(封装Cancellable,避免重复代码)
class BaseViewModel {
/// 存储Combine订阅,防止销毁
var cancellables = Set<AnyCancellable>()
/// 加载状态(View层绑定,控制加载动画)
@Published var isLoading = false
/// 错误信息(View层绑定,展示错误提示)
@Published var errorMsg = ""
}
// 用户模块ViewModel
class UserViewModel: BaseViewModel {
/// 发布用户信息(View层绑定,更新UI)
@Published var userInfo: UserModel?
/// 发布登录成功的Token(View层绑定,处理后续逻辑)
@Published var loginToken: String?
// MARK: - 1. 用户登录(调用UserAPI.login)
func login(account: String, password: String) {
// 校验参数(本地业务逻辑,非网络层职责)
guard !account.isEmpty, !password.isEmpty else {
errorMsg = "账号或密码不能为空"
return
}
// 开始加载
isLoading = true
// 一行代码发起请求,订阅结果(核心!)
RESTfulRequestTool.request(UserAPI.login(account: account, password: password))
.sinkRESTful(to: &cancellables) { [weak self] loginResult in
// 登录成功回调(loginResult为强类型LoginModel)
self?.isLoading = false
self?.loginToken = loginResult.token
// 1. 保存Token到本地(UserDefaults/Keychain)
UserDefaults.standard.set(loginResult.token, forKey: "kUserToken")
// 2. 动态更新全局请求头的Token,后续请求自动带上
NetworkConfig.shared.updateGlobalHeaders(["Token": loginResult.token])
// 3. 重新初始化MoyaProvider,让新Token生效
NetworkManager.shared.reloadProvider()
// 4. 登录成功后,获取用户信息
self?.fetchUser(id: loginResult.userId)
} onFailure: { [weak self] error in
// 失败回调(error为统一的NetworkError)
self?.isLoading = false
self?.errorMsg = error.errorDescription ?? "登录失败"
// 处理Token过期(发送全局通知,跳转到登录页)
if error.isTokenExpired {
NotificationCenter.default.post(name: NSNotification.Name("kRESTfulTokenExpired"), object: nil)
}
}
}
// MARK: - 2. 获取用户信息(调用UserAPI.fetchUser)
func fetchUser(id: String) {
isLoading = true
// 一行代码发起请求,自定义配置(如重试1次)
let config = RESTfulRequestConfig(retryCount: 1)
RESTfulRequestTool.request(UserAPI.fetchUser(id: id), config: config)
.sinkRESTful(to: &cancellables) { [weak self] user in
self?.isLoading = false
self?.userInfo = user // 赋值给发布属性,View层自动更新UI
} onFailure: { [weak self] error in
self?.isLoading = false
self?.errorMsg = error.errorDescription ?? "获取用户信息失败"
if error.isTokenExpired {
NotificationCenter.default.post(name: NSNotification.Name("kRESTfulTokenExpired"), object: nil)
}
}
}
}
// MARK: - 业务数据模型(需遵守Codable,和服务端data字段格式一致)
/// 登录返回模型
struct LoginModel: Codable {
let token: String
let userId: String
let userName: String
}
/// 用户信息模型
struct UserModel: Codable {
let id: String
let name: String
let avatar: String
let phone: String
let email: String
let createTime: String
}
6.2 View 层(UIKit ViewController)
无任何业务逻辑 ,只需绑定 ViewModel 的 @Published 属性,响应数据变化更新 UI,处理用户交互(如按钮点击),完全符合 MVVM 的设计思想。
swift
import UIKit
import Combine
class UserViewController: UIViewController {
// 绑定ViewModel
private let vm = UserViewModel()
// 存储Combine订阅
private var cancellables = Set<AnyCancellable>()
// UI控件(示例)
@IBOutlet weak var accountTF: UITextField!
@IBOutlet weak var pwdTF: UITextField!
@IBOutlet weak var loginBtn: UIButton!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var avatarIV: UIImageView!
@IBOutlet weak var loadingView: UIActivityIndicatorView!
@IBOutlet weak var errorLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// 绑定ViewModel的属性到UI(核心!数据驱动UI)
bindViewModel()
// 配置UI控件
setupUI()
}
// MARK: - 数据绑定(Combine自动响应,无需手动刷新UI)
private func bindViewModel() {
// 1. 绑定加载状态:控制加载动画
vm.$isLoading
.sink { [weak self] isLoading in
isLoading ? self?.loadingView.startAnimating() : self?.loadingView.stopAnimating()
self?.loginBtn.isEnabled = !isLoading // 加载时禁用按钮
}
.store(in: &cancellables)
// 2. 绑定错误信息:展示错误提示
vm.$errorMsg
.sink { [weak self] msg in
self?.errorLabel.text = msg
self?.errorLabel.isHidden = msg.isEmpty
}
.store(in: &cancellables)
// 3. 绑定用户信息:更新UI
vm.$userInfo
.compactMap { $0 } // 过滤nil
.sink { [weak self] user in
self?.nameLabel.text = user.name
// 加载头像(使用Kingfisher/SDWebImage,示例)
if let avatarURL = URL(string: user.avatar) {
self?.avatarIV.kf.setImage(with: avatarURL, placeholder: UIImage(named: "avatar_default"))
}
}
.store(in: &cancellables)
// 4. 绑定登录成功:可做后续逻辑(如返回上一页、跳转到首页)
vm.$loginToken
.compactMap { $0 }
.sink { [weak self] _ in
self?.showToast(message: "登录成功")
// 跳转到首页
self?.gotoHomePage()
}
.store(in: &cancellables)
}
// MARK: - UI配置
private func setupUI() {
loadingView.hidesWhenStopped = true
errorLabel.textColor = .red
loginBtn.layer.cornerRadius = 8
}
// MARK: - 事件处理:登录按钮点击
@IBAction func loginBtnClick(_ sender: UIButton) {
view.endEditing(true)
// 调用ViewModel的登录方法,无需接触网络层
vm.login(
account: accountTF.text ?? "",
password: pwdTF.text ?? ""
)
}
// 跳转到首页(示例)
private func gotoHomePage() {
let homeVC = HomeViewController()
navigationController?.pushViewController(homeVC, animated: true)
}
// 显示吐司(示例,可使用第三方库如MBProgressHUD)
private func showToast(message: String) {
let toast = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 40))
toast.text = message
toast.textAlignment = .center
toast.backgroundColor = .black.withAlphaComponent(0.7)
toast.textColor = .white
toast.layer.cornerRadius = 20
toast.clipsToBounds = true
toast.center = view.center
view.addSubview(toast)
UIView.animate(withDuration: 2, animations: {
toast.alpha = 0
}) { _ in
toast.removeFromSuperview()
}
}
}
七、框架进阶扩展(生产级必备)
以上封装是基础可用版 ,可根据企业级项目需求,在不修改业务层代码 的前提下,快速扩展以下生产级能力 ------ 框架的模块化设计保证了开闭原则(对扩展开放,对修改关闭)。
7.1 网络状态检测
添加无网络拦截 ,在请求前检查网络状态,无网络时直接返回.noNetwork错误,避免无用的网络请求:
- 引入网络状态库:
pod 'ReachabilitySwift', '~> 5.0'; - 在
NetworkManager中添加Reachability单例,监听网络状态; - 创建网络状态插件 (遵循
PluginType),在willSend中检查网络,无网络时取消请求并抛错; - 将网络状态插件添加到
createMoyaPlugins的插件数组中。
7.2 文件上传 / 下载封装
基于BaseAPI的uploadTask/downloadTask方法,快速封装文件上传(如图片、视频)和下载功能,示例(头像上传):
swift
// 1. 在UserAPI中添加上传枚举
enum UserAPI: BaseAPI {
case uploadAvatar(data: Data, fileName: String) // 上传头像
var path: String { "/user/avatar/upload" }
var method: Moya.Method { .post }
var parameters: [String: Any]? { nil }
// 重写task,使用上传任务
var task: Task {
switch self {
case .uploadAvatar(let data, let name):
let formData = MultipartFormData(provider: .data(data), name: "avatar", fileName: name, mimeType: "image/jpeg")
return uploadTask(with: [formData])
default:
return .requestParameters(parameters: parameters ?? [:], encoding: parameterEncoding)
}
}
}
// 2. 业务层一行代码发起上传请求
RESTfulRequestTool.request(UserAPI.uploadAvatar(data: imageData, fileName: "avatar.jpg"))
.sinkRESTful(...)
7.3 全局请求加密 / 响应解密
添加加密 / 解密插件,实现全局请求参数加密、响应数据解密,无需业务层处理:
- 封装加密工具类(如 AES/RSA):
CryptoTool.encrypt(params:)/CryptoTool.decrypt(data:); - 创建加密插件 ,在
willSend中对请求参数进行加密; - 创建解密插件 ,在
didReceive中对响应数据进行解密; - 将插件添加到
NetworkManager的插件数组中,注意插件执行顺序(加密在前,解密在后)。
7.4 Token 自动刷新
实现Token 过期自动刷新,无需用户重新登录,提升体验:
- 在
NetworkError的tokenExpired回调中,发起刷新 Token 请求(使用刷新 Token); - 刷新成功后,更新全局请求头的 Token 并重新初始化 MoyaProvider;
- 重新发起之前失败的请求 (可通过 Moya 的
RequestRetrier实现); - 刷新失败时,再发送全局通知跳转到登录页。
7.5 自定义缓存策略
基于 Alamofire 的URLRequest.CachePolicy和 iOS 的URLCache,封装业务级缓存(如列表页缓存、个人信息缓存):
- 在
RESTfulRequestConfig中添加cacheKey(缓存键)和cacheExpire(缓存过期时间); - 创建缓存工具类 ,基于
UserDefaults/CoreData实现缓存的增删改查; - 创建缓存插件,在请求前检查缓存(未过期则直接返回缓存数据),请求成功后更新缓存。
7.6 统一错误弹窗 / 吐司
封装全局错误处理工具 ,在NetworkError的失败回调中,根据错误类型自动展示全局弹窗 / 吐司,业务层无需重复写弹窗逻辑:
swift
// 封装全局错误工具
struct NetworkErrorTool {
static func handleError(_ error: NetworkError) {
switch error {
case .noNetwork, .tokenExpired, .serverError:
// 展示全局吐司
HUDManager.showToast(error.errorDescription!)
case .jsonParseError, .invalidBaseURL:
// 开发环境打印日志,生产环境隐藏
#if DEBUG
HUDManager.showAlert(title: "错误", message: error.errorDescription!)
#endif
default:
break
}
}
}
// 业务层使用(一行代码处理错误)
.sinkRESTful(to: &cancellables) { data in
// 成功处理
} onFailure: { error in
NetworkErrorTool.handleError(error)
}
7.7 多 BaseURL 支持
若项目需要访问多个 RESTful 服务 (如主服务、支付服务、统计服务),只需修改BaseAPI,添加customBaseURL属性,让不同模块的 API 使用不同的基础 URL:
swift
// 1. 在BaseAPI中添加自定义基础URL属性
protocol BaseAPI: TargetType {
var customBaseURL: String? { get } // 自定义基础URL(nil则使用全局)
// ... 其他属性
}
// 2. 重写baseURL方法
extension BaseAPI {
var baseURL: URL {
if let customURL = customBaseURL, let url = URL(string: customURL) {
return url
}
guard let url = URL(string: NetworkConfig.shared.baseURL) else {
fatalError(NetworkError.invalidBaseURL.errorDescription!)
}
return url
}
}
// 3. 支付模块API使用自定义基础URL
enum PayAPI: BaseAPI {
case payOrder(orderId: String)
var customBaseURL: String? { "https://pay-api.xxx.com/v1" } // 支付服务基础URL
var path: String { "/pay/order" }
var method: Moya.Method { .post }
// ... 其他属性
}
7.8 防止重复请求
Moya 的trackInflights: true可防止相同 URL + 参数 + 方法的重复请求,若需要更精细的控制(如按业务标识防重),可:
- 在
BaseAPI中添加uniqueKey属性(业务唯一标识,如"user_login"); - 在
NetworkManager中维护一个请求标识集合,记录正在进行的请求; - 创建防重插件 ,在
willSend中检查标识,已存在则取消请求。
八、框架核心优势
这套基于 Moya+Alamofire 的 RESTful 网络框架,贴合 iOS 开发最佳实践,相比直接使用 Alamofire,具备以下核心优势,尤其适合中大型企业级项目:
- 请求高度解耦:用枚举集中管理所有 RESTful 请求,URL / 参数 / 方法不再分散,修改、维护、排查问题更高效;
- 开发效率极高:业务层只需编写 API 枚举和数据模型,一行代码发起请求,自动解析 JSON,无需手动处理解析错误;
- 错误统一处理 :所有异常(网络、解析、业务、无网络)归一为
NetworkError,业务层只需处理一种错误类型,避免冗余代码; - 全局配置灵活:单例管理多环境、请求头、超时,支持运行时动态更新(如 Token 刷新、环境切换),无需重启 App;
- 贴合 MVVM/Combine:基于 Combine 实现响应式请求,支持数据绑定,完美适配现代 iOS 开发架构,实现视图与业务解耦;
- 可扩展性极强:模块化设计,支持插件化扩展(日志、加密、缓存、网络状态),扩展不影响业务层代码;
- 调试友好:DEBUG 模式下打印详细的请求 / 响应日志,RELEASE 模式自动关闭,避免生产环境信息泄露;
- 生态成熟:基于 Moya+Alamofire 的事实标准,社区解决方案丰富,团队学习成本低,问题易排查。
九、使用注意事项
- 数据模型对齐 :业务模型(如
UserModel)必须和服务端data字段的 JSON 格式严格一致,否则会触发jsonParseError; - 多环境配置 :需在 Xcode 中配置Build Configuration (Debug/Test/Release),配合
configEnvFromBuildConfig实现自动环境切换; - Token 安全性 :建议将 Token 保存在Keychain 中(而非
UserDefaults),UserDefaults为明文存储,存在安全风险; - 避免循环引用 :所有 Combine/RxSwift 闭包中必须使用
[weak self],框架和业务层均需遵循,防止内存泄漏; - 重复请求控制 :开启
trackInflights: true防止相同请求重复发起,高频点击按钮(如登录)需在 View 层添加点击防抖; - 参数编码 :表单提交(如传统网页请求)需将
parameterEncoding改为URLEncoding.default,JSON 提交使用默认的JSONEncoding.default; - Moya 版本 :本文基于 Moya 15.x+,若使用旧版本(如 14.x),需微调
TargetType的部分 API(如task的类型)。
总结
这套框架是生产级可直接落地 的 RESTful 网络封装方案,核心是在保留 Moya+Alamofire 核心优势的基础上,做高度的模块化、易用性封装,解决了 RESTful 开发中「请求分散、解析繁琐、错误处理混乱」的痛点。
框架的核心设计思路 是:配置层统一管理全局参数、基础层封装通用模型和错误、核心层处理 Moya 请求和解析、工具层暴露极简 API ,贴合 iOS 现代开发思想(MVVM/Combine/RxSwift),是传统业务 App、工具类 App、接口简单且固定的项目的最优选择 ------ 完全适配 RESTful「资源驱动、HTTP 标准」的特性,同时兼顾开发效率、可维护性和可扩展性。