要基于 iOS 平台的 Apollo GraphQL 客户端,结合 RxSwift/RxCocoa 封装一套高复用、易维护、贴合响应式编程思想的网络框架,核心需求是解耦 Apollo 原生回调与 Rx 的可观察序列 、统一异常处理 、封装通用请求层 、支持请求配置 / 取消 / 复用,同时遵循 iOS 开发的最佳实践(如模块化、单例管理、泛型约束)。
以下是一套完整的封装方案,从环境准备 →核心层封装 →业务层扩展 →使用示例 →进阶优化逐步实现,代码可直接在项目中落地,兼容最新的 Apollo iOS(1.0+)和 RxSwift 6.x+。
一、环境准备
首先在Podfile中引入核心依赖,确保版本兼容(Apollo 1.0+ 已适配 Swift 5.5+,RxSwift 6.x+ 是主流稳定版):
ruby
pod 'Apollo', '~> 1.0' # GraphQL核心客户端
pod 'Apollo/SQLite', '~> 1.0' # 可选,本地缓存(如需要离线查询)
pod 'RxSwift', '~> 6.0' # 响应式核心
pod 'RxCocoa', '~> 6.0' # 结合UI的响应式扩展
pod 'RxRelay', '~> 6.0' # 无误差的可观察序列(可选,用于状态管理)
pod 'Moya/ReactiveSwift', '~> 15.0' # 可选,若项目同时有RESTful请求可统一管理
执行pod install后,完成 Apollo 的基础配置(关键 :Apollo 需要生成 GraphQL 的 Model 代码,需在Build Phases添加脚本,参考Apollo 官方配置)。
二、核心设计思路
- 单例管理 ApolloClient:全局统一配置 GraphQL 的 BaseURL、请求头、缓存、超时等,避免多次初始化;
- Rx 封装 Apollo 请求 :将 Apollo 原生的
completionHandler回调转换为Observable/Single(GraphQL 请求单次结果,优先用 Single); - 统一异常处理:封装自定义网络错误枚举,覆盖 Apollo 原生错误、网络错误、解析错误、业务错误等;
- 泛型约束请求层 :支持所有 Apollo 生成的
GraphQLQuery/GraphQLMutation(查询 / 修改操作),做到一次封装、全局复用; - 支持请求配置:支持单个请求自定义头、超时、缓存策略,覆盖全局配置;
- 贴合 Rx 生命周期 :请求自动绑定到
DisposeBag,支持手动取消,避免内存泄漏; - 可选的请求拦截:支持请求前拦截(如添加 Token)、响应后拦截(如统一处理 Token 过期)。
三、完整框架封装
按模块化 拆分文件,建议在项目中创建Network/GraphQL目录,包含以下核心文件:
plaintext
GraphQL/
├── ApolloManager.swift # ApolloClient单例管理、全局配置
├── ApolloRxExtension.swift # Apollo请求的RxSwift扩展(核心)
├── GraphQLRequest.swift # 通用请求封装、配置模型
├── GraphQLResult.swift # 统一响应结果、自定义错误
└── GraphQLInterceptor.swift # 可选,请求/响应拦截器
3.1 自定义错误与统一结果(GraphQLResult.swift)
封装全局通用的错误枚举,覆盖 Apollo 所有可能的错误类型,同时统一响应结果格式,让业务层无需处理原生 Apollo 结果:
swift
import Foundation
import Apollo
import RxSwift
// MARK: - 自定义GraphQL网络错误
enum GraphQLRequestError: Error, LocalizedError {
case invalidURL // 无效BaseURL
case noNetwork // 无网络(可结合Reachability)
case apolloError(ApolloError) // Apollo原生错误(请求、解析、服务端)
case serverError(code: Int, message: String) // 服务端业务错误(如code≠200)
case emptyResponse // 响应体为空
case tokenExpired // Token过期(自定义业务)
case customError(String) // 自定义其他错误
// 本地化描述(方便调试/提示)
var errorDescription: String? {
switch self {
case .invalidURL:
return "无效的GraphQL请求地址"
case .noNetwork:
return "网络连接失败,请检查网络"
case .apolloError(let error):
return "Apollo请求错误:\(error.localizedDescription)"
case .serverError(let code, let message):
return "服务端错误[\(code)]:\(message)"
case .emptyResponse:
return "服务端返回空数据"
case .tokenExpired:
return "登录状态过期,请重新登录"
case .customError(let msg):
return msg
}
}
}
// MARK: - 统一GraphQL响应结果
/// 泛型封装,T为Apollo生成的GraphQL数据模型
struct GraphQLResult<T> {
let data: T? // 业务数据
let response: HTTPURLResponse? // 原始响应头
let extensions: [String: Any]? // 服务端扩展字段(如traceId、code)
}
// MARK: - 无数据响应(用于Mutation无返回值的场景)
struct EmptyGraphQLData: Codable {}
3.2 ApolloClient 单例管理(ApolloManager.swift)
全局唯一管理 ApolloClient,配置 BaseURL、请求头、缓存、超时,提供全局配置入口 和客户端获取方法,支持运行时动态修改请求头(如 Token 刷新后更新):
swift
import Foundation
import Apollo
import Apollo/SQLite
// MARK: - Apollo全局管理单例
final class ApolloManager {
// 单例(线程安全)
static let shared = ApolloManager()
private init() {}
// 核心ApolloClient(懒加载)
private lazy var apolloClient: ApolloClient = {
guard let url = URL(string: baseURL) else {
fatalError(GraphQLRequestError.invalidURL.errorDescription!)
}
// 1. 创建网络传输层(配置超时、请求头)
let transport = RequestChainNetworkTransport(
interceptorProvider: DefaultInterceptorProvider(client: self),
endpointURL: url,
requestBodyCreator: ApolloRequestBodyCreator(),
timeout: globalTimeout
)
// 2. 可选:配置本地缓存(SQLite),支持离线查询
let cache = SQLiteNormalizedCache(fileURL: cacheFileURL)
let store = ApolloStore(cache: cache)
// 3. 初始化ApolloClient
let client = ApolloClient(networkTransport: transport, store: store)
return client
}()
// MARK: - 全局配置项(可在AppDelegate/SceneDelegate中初始化)
var baseURL: String = "" // GraphQL服务地址,如https://xxx.com/graphql
var globalTimeout: TimeInterval = 30 // 全局超时时间
var globalHeaders: [String: String] = [:] // 全局请求头,如Token、AppVersion
/// 本地缓存文件路径(可选)
private var cacheFileURL: URL {
let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
return URL(fileURLWithPath: documents).appendingPathComponent("ApolloCache.sqlite")
}
// MARK: - 对外提供客户端(避免直接访问私有属性)
func getClient() -> ApolloClient {
return apolloClient
}
// MARK: - 动态更新全局请求头(如Token刷新后)
func updateGlobalHeaders(_ headers: [String: String]) {
globalHeaders.merge(headers) { _, new in new }
// 重新初始化传输层,让新的请求头生效
guard let url = URL(string: baseURL) else { return }
let newTransport = RequestChainNetworkTransport(
interceptorProvider: DefaultInterceptorProvider(client: self),
endpointURL: url,
requestBodyCreator: ApolloRequestBodyCreator(),
timeout: globalTimeout
)
apolloClient.networkTransport = newTransport
}
}
// MARK: - 自定义拦截器提供者(注入全局请求头)
extension ApolloManager: InterceptorProvider {
func interceptors<Operation>(for operation: Operation) -> [ApolloInterceptor] where Operation : GraphQLOperation {
var interceptors = DefaultInterceptorProvider().interceptors(for: operation)
// 插入自定义请求头拦截器(在所有拦截器最前面)
interceptors.insert(GlobalHeaderInterceptor(), at: 0)
return interceptors
}
}
// MARK: - 全局请求头拦截器(核心:将全局Headers注入请求)
final class GlobalHeaderInterceptor: ApolloInterceptor {
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
// 注入全局请求头
let globalHeaders = ApolloManager.shared.globalHeaders
globalHeaders.forEach { key, value in
request.addHeader(value, for: key)
}
// 继续执行拦截器链
chain.proceedAsync(request: request, response: response, completion: completion)
}
}
3.3 Apollo 的 RxSwift 核心扩展(ApolloRxExtension.swift)
最核心的文件 :将 Apollo 原生的闭包回调 转换为 RxSwift 的Single可观察序列,实现响应式的请求封装,同时统一异常处理,让业务层直接订阅结果即可:
swift
import Foundation
import Apollo
import RxSwift
import RxCocoa
// MARK: - ApolloClient的RxSwift扩展(支持Query/Mutation)
extension ApolloClient {
/// 执行GraphQL查询(Query),返回Single可观察序列
/// - Parameter query: Apollo生成的Query模型(遵守GraphQLQuery协议)
/// - Parameter config: 单个请求的自定义配置(可选,覆盖全局)
func rx_fetch<Query: GraphQLQuery>(
query: Query,
config: GraphQLRequestConfig? = nil
) -> Single<GraphQLResult<Query.Data>> {
return Single.create { [weak self] single in
guard let self = self else {
single(.failure(GraphQLRequestError.customError("ApolloClient已释放")))
return Disposables.create()
}
// 1. 构建请求选项(支持自定义缓存、队列)
let cachePolicy = config?.cachePolicy ?? .returnCacheDataAndFetch
let queue = DispatchQueue.global(qos: .default)
// 2. 执行Apollo原生请求
let cancellable = self.fetch(
query: query,
cachePolicy: cachePolicy,
queue: queue,
resultHandler: { [weak self] result in
self?.handleApolloResult(result, single: single)
}
)
// 3. 返回取消器(Rx销毁时自动取消请求)
return Disposables.create {
cancellable.cancel()
}
}
// 切换到主线程(方便业务层直接更新UI)
.observe(on: MainScheduler.instance)
// 统一超时处理(覆盖全局/自定义超时)
.timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: MainScheduler.instance)
}
/// 执行GraphQL修改(Mutation),返回Single可观察序列
/// - Parameter mutation: Apollo生成的Mutation模型(遵守GraphQLMutation协议)
/// - Parameter config: 单个请求的自定义配置(可选,覆盖全局)
func rx_perform<Mutation: GraphQLMutation>(
mutation: Mutation,
config: GraphQLRequestConfig? = nil
) -> Single<GraphQLResult<Mutation.Data>> {
return Single.create { [weak self] single in
guard let self = self else {
single(.failure(GraphQLRequestError.customError("ApolloClient已释放")))
return Disposables.create()
}
let queue = DispatchQueue.global(qos: .default)
// 执行Apollo原生Mutation
let cancellable = self.perform(
mutation: mutation,
queue: queue,
resultHandler: { [weak self] result in
self?.handleApolloResult(result, single: single)
}
)
// 返回取消器
return Disposables.create {
cancellable.cancel()
}
}
.observe(on: MainScheduler.instance)
.timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: MainScheduler.instance)
}
// MARK: - 统一处理Apollo原生结果,转换为自定义Result/Error
private func handleApolloResult<Operation: GraphQLOperation>(
_ result: Result<GraphQLResult<Operation.Data>, Error>,
single: @escaping (SingleEvent<GraphQLResult<Operation.Data>>) -> Void
) {
switch result {
case .success(let apolloResult):
// 1. 检查服务端返回的错误(Apollo的errors字段)
if let graphQLErrors = apolloResult.errors, !graphQLErrors.isEmpty {
let firstError = graphQLErrors.first!
let code = firstError.extensions?["code"] as? Int ?? -1
let message = firstError.message ?? "服务端GraphQL错误"
// 自定义业务错误:Token过期(示例,根据后端约定调整)
if code == 401 {
single(.failure(GraphQLRequestError.tokenExpired))
} else {
single(.failure(GraphQLRequestError.serverError(code: code, message: message)))
}
} else if apolloResult.data == nil {
// 2. 数据为空
single(.failure(GraphQLRequestError.emptyResponse))
} else {
// 3. 请求成功,封装为自定义统一结果
let customResult = GraphQLResult(
data: apolloResult.data,
response: apolloResult.response,
extensions: apolloResult.extensions
)
single(.success(customResult))
}
case .failure(let error):
// 4. Apollo原生错误(网络、解析、超时等)
if let apolloError = error as? ApolloError {
single(.failure(GraphQLRequestError.apolloError(apolloError)))
} else {
single(.failure(GraphQLRequestError.customError(error.localizedDescription)))
}
}
}
}
3.4 通用请求配置模型(GraphQLRequest.swift)
封装单个请求的自定义配置,支持覆盖全局的超时、缓存策略、请求头,让框架更灵活(比如某些请求需要单独的 Token / 超时):
swift
import Foundation
import Apollo
// MARK: - GraphQL单个请求的自定义配置
struct GraphQLRequestConfig {
var timeout: TimeInterval? // 单个请求超时(nil则使用全局)
var cachePolicy: CachePolicy = .returnCacheDataAndFetch // 缓存策略(Apollo原生)
var customHeaders: [String: String]? // 单个请求自定义头(覆盖全局同Key)
var isIgnoreToken: Bool = false // 是否忽略全局Token(如登录/注册请求)
}
// MARK: - 通用GraphQL请求工具(可选,进一步简化业务层调用)
struct GraphQLRequest {
/// 通用查询方法
static func fetch<Query: GraphQLQuery>(
_ query: Query,
config: GraphQLRequestConfig? = nil
) -> Single<GraphQLResult<Query.Data>> {
return ApolloManager.shared.getClient().rx_fetch(query: query, config: config)
}
/// 通用修改方法
static func perform<Mutation: GraphQLMutation>(
_ mutation: Mutation,
config: GraphQLRequestConfig? = nil
) -> Single<GraphQLResult<Mutation.Data>> {
return ApolloManager.shared.getClient().rx_perform(mutation: mutation, config: config)
}
}
3.5 可选:请求 / 响应拦截器(GraphQLInterceptor.swift)
封装全局拦截逻辑,比如统一处理 Token 过期、网络状态检查、请求日志打印,避免在业务层重复写逻辑,这里实现两个常用拦截器:
swift
import Foundation
import Apollo
import RxSwift
import SystemConfiguration
// MARK: - 网络状态检查拦截器(全局)
final class NetworkReachabilityInterceptor: ApolloInterceptor {
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
if !isNetworkReachable() {
completion(.failure(GraphQLRequestError.noNetwork))
} else {
chain.proceedAsync(request: request, response: response, completion: completion)
}
}
// 简单的网络状态检查(可替换为ReachabilitySwift库,更精准)
private func isNetworkReachable() -> Bool {
var zeroAddress = sockaddr_in()
zeroAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
zeroAddress.sin_family = sa_family_t(AF_INET)
guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
SCNetworkReachabilityCreateWithAddress(nil, $0)
}
}) else { return false }
var flags: SCNetworkReachabilityFlags = []
if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { return false }
let isReachable = flags.contains(.reachable)
let needsConnection = flags.contains(.connectionRequired)
return isReachable && !needsConnection
}
}
// MARK: - Token过期全局拦截器(响应后)
final class TokenExpiredInterceptor: ApolloInterceptor {
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
// 先执行后续拦截器,再处理响应
chain.proceedAsync(request: request, response: response) { [weak self] result in
self?.handleTokenExpired(result, completion: completion)
}
}
private func handleTokenExpired<Operation>(
_ result: Result<GraphQLResult<Operation.Data>, Error>,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
if case .failure(let error) = result, error is GraphQLRequestError,
(error as! GraphQLRequestError) == .tokenExpired {
// 全局处理Token过期:如发送通知、跳转到登录页
NotificationCenter.default.post(name: NSNotification.Name("kTokenExpiredNotification"), object: nil)
completion(result)
} else {
completion(result)
}
}
}
// MARK: - 日志打印拦截器(调试用)
final class LogInterceptor: ApolloInterceptor {
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
// 打印请求信息
#if DEBUG
print("📡 GraphQL Request: \(request.operation.operationName) | \(request.url?.absoluteString ?? "")")
print("📋 Request Headers: \(request.allHTTPHeaderFields ?? [:])")
if let body = request.httpBody, let bodyStr = String(data: body, encoding: .utf8) {
print("📝 Request Body: \(bodyStr)")
}
#endif
// 执行后续拦截器
chain.proceedAsync(request: request, response: response) { result in
// 打印响应信息
#if DEBUG
switch result {
case .success(let res):
print("✅ GraphQL Success: \(request.operation.operationName) | Data: \(res.data ?? "nil")")
case .failure(let error):
print("❌ GraphQL Failure: \(request.operation.operationName) | Error: \(error.localizedDescription)")
}
#endif
completion(result)
}
}
}
// MARK: - 给ApolloManager添加拦截器(在InterceptorProvider中注入)
extension ApolloManager {
/// 初始化所有全局拦截器(可在App启动时调用)
func setupGlobalInterceptors() {
// 注意:拦截器顺序很重要!请求前拦截→日志→头信息→网络检查→响应后拦截
// 可根据需求调整顺序
}
}
使用拦截器 :在ApolloManager的interceptors(for:)方法中,将上述拦截器插入到拦截器链中即可。
四、框架初始化与全局配置
在 App 启动时(AppDelegate.application(_:didFinishLaunchingWithOptions:)或SceneDelegate.scene(_:willConnectTo:options:))初始化 Apollo 全局配置,这是框架使用的前提:
swift
import UIKit
import RxSwift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let disposeBag = DisposeBag()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. 初始化Apollo全局配置
let apolloManager = ApolloManager.shared
apolloManager.baseURL = "https://your-server.com/graphql" // 替换为你的GraphQL服务地址
apolloManager.globalTimeout = 20
apolloManager.globalHeaders = [
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"Device-Type": "iOS",
"Token": UserDefaults.standard.string(forKey: "kUserToken") ?? "" // 从本地获取Token
]
// 2. 监听Token过期通知,全局处理(如跳转到登录页)
NotificationCenter.default.rx.notification(NSNotification.Name("kTokenExpiredNotification"))
.takeUntil(rx.deallocated) // 绑定到AppDelegate生命周期
.subscribe(onNext: { [weak self] _ in
self?.gotoLoginPage()
})
.disposed(by: disposeBag)
return true
}
// Token过期跳转到登录页
private func gotoLoginPage() {
let loginVC = LoginViewController()
let nav = UINavigationController(rootViewController: loginVC)
window?.rootViewController = nav
window?.makeKeyAndVisible()
// 清除本地Token
UserDefaults.standard.removeObject(forKey: "kUserToken")
}
}
五、业务层使用示例
假设你已经通过 Apollo 生成了 GraphQL 的 Model 代码(如UserQuery:查询用户信息,LoginMutation:用户登录),业务层(如 VM 层)可以一行代码发起请求,完全贴合 RxSwift 的响应式编程,无需处理任何原生 Apollo 逻辑。
5.1 基础使用:查询用户信息(Query)
swift
import UIKit
import RxSwift
import RxCocoa
class UserViewModel {
let disposeBag = DisposeBag()
// 用于绑定UI的用户信息(RxCocoa,方便更新UI)
let userInfo = PublishRelay<UserQuery.Data.User?>()
// 用于绑定UI的错误提示
let errorMsg = PublishRelay<String>()
/// 查询用户信息
func fetchUserInfo(userId: String) {
// 1. 构建Apollo生成的Query(传入参数)
let query = UserQuery(id: userId)
// 2. 自定义请求配置(可选,nil则使用全局)
let config = GraphQLRequestConfig(
timeout: 15,
cachePolicy: .fetchIgnoringCacheData
)
// 3. 发起请求,订阅结果
GraphQLRequest.fetch(query, config: config)
.subscribe(onSuccess: { [weak self] result in
// 请求成功,将数据发送到Relay,UI层绑定即可
self?.userInfo.accept(result.data?.user)
}, onFailure: { [weak self] error in
// 请求失败,发送错误信息
self?.errorMsg.accept(error.localizedDescription)
})
.disposed(by: disposeBag)
}
}
// UI层绑定(ViewController)
class UserViewController: UIViewController {
let vm = UserViewModel()
let disposeBag = DisposeBag()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var tipLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
bindVM()
// 发起请求
vm.fetchUserInfo(userId: "123456")
}
private func bindVM() {
// 绑定用户名称到Label
vm.userInfo
.compactMap { $0?.name }
.bind(to: nameLabel.rx.text)
.disposed(by: disposeBag)
// 绑定错误信息到TipLabel
vm.errorMsg
.bind(to: tipLabel.rx.text)
.disposed(by: disposeBag)
}
}
5.2 基础使用:用户登录(Mutation)
swift
class LoginViewModel {
let disposeBag = DisposeBag()
let loginSuccess = PublishRelay<LoginMutation.Data.Login?>()
let errorMsg = PublishRelay<String>()
/// 用户登录
func login(account: String, pwd: String) {
let mutation = LoginMutation(account: account, password: pwd)
// 登录请求忽略全局Token(自定义配置)
let config = GraphQLRequestConfig(isIgnoreToken: true)
GraphQLRequest.perform(mutation, config: config)
.subscribe(onSuccess: { [weak self] result in
guard let loginData = result.data?.login else { return }
// 登录成功,保存Token到本地
UserDefaults.standard.set(loginData.token, forKey: "kUserToken")
// 更新Apollo全局Token
ApolloManager.shared.updateGlobalHeaders(["Token": loginData.token])
// 发送登录成功事件
self?.loginSuccess.accept(loginData)
}, onFailure: { [weak self] error in
self?.errorMsg.accept(error.localizedDescription)
})
.disposed(by: disposeBag)
}
}
5.3 高级使用:请求取消 / 复用
RxSwift 的Disposable天然支持请求取消,只需将订阅的Disposable保存,需要时调用dispose()即可:
swift
class DemoViewModel {
let disposeBag = DisposeBag()
var fetchDisposable: Disposable? // 保存请求的Disposable
func fetchData() {
// 先取消之前的请求,避免重复请求
fetchDisposable?.dispose()
let query = DemoQuery()
fetchDisposable = GraphQLRequest.fetch(query)
.subscribe(onSuccess: { data in
// 处理数据
}, onFailure: { error in
// 处理错误
})
fetchDisposable?.disposed(by: disposeBag)
}
// 手动取消请求(如页面返回时)
func cancelFetch() {
fetchDisposable?.dispose()
}
}
六、进阶优化与扩展
6.1 结合 MVVM+Coordinator
将网络请求完全放在VM 层 ,VM 层通过Relay/Subject将数据发送到 VC 层,VC 层只负责绑定 UI,不处理任何业务逻辑,符合 MVVM 的设计思想。
6.2 添加请求加载状态
封装全局的加载状态管理,比如通过BehaviorRelay<Bool>管理请求的加载状态,UI 层绑定到 MBProgressHUD 等加载控件:
swift
class BaseViewModel {
let disposeBag = DisposeBag()
let isLoading = BehaviorRelay<Bool>(value: false) // 全局加载状态
}
class UserViewModel: BaseViewModel {
func fetchUserInfo(userId: String) {
isLoading.accept(true) // 开始加载
let query = UserQuery(id: userId)
GraphQLRequest.fetch(query)
.subscribe(onSuccess: { [weak self] result in
self?.userInfo.accept(result.data?.user)
self?.isLoading.accept(false) // 结束加载
}, onFailure: { [weak self] error in
self?.errorMsg.accept(error.localizedDescription)
self?.isLoading.accept(false) // 结束加载
})
.disposed(by: disposeBag)
}
}
6.3 批量请求处理
结合 RxSwift 的zip/combineLatest实现批量请求,等待所有请求完成后再处理结果:
swift
// 同时查询用户信息和商品列表,都完成后再更新UI
Observable.zip(
GraphQLRequest.fetch(UserQuery(id: "123")),
GraphQLRequest.fetch(GoodsListQuery(page: 1))
)
.subscribe(onNext: { userResult, goodsResult in
// 处理用户信息和商品列表
let user = userResult.data?.user
let goods = goodsResult.data?.goodsList
})
.disposed(by: disposeBag)
6.4 离线缓存优化
Apollo 自带强大的本地缓存能力,可通过cachePolicy配置不同的缓存策略,比如:
.returnCacheDataAndFetch:先返回缓存数据,再请求网络更新(适合列表页).fetchIgnoringCacheData:忽略缓存,强制请求网络(适合登录 / 支付等敏感操作).returnCacheDataDontFetch:只从缓存获取,不请求网络(适合离线场景)
6.5 错误重试机制
结合 RxSwift 的retry/retryWhen实现错误重试,比如网络波动时自动重试 3 次:
swift
GraphQLRequest.fetch(UserQuery(id: "123"))
.retry(3) // 失败后自动重试3次
.subscribe(...)
.disposed(by: disposeBag)
// 高级:带延迟的重试(失败后延迟1秒重试,共3次)
GraphQLRequest.fetch(UserQuery(id: "123"))
.retryWhen { errorObservable in
errorObservable.flatMapWithIndex { error, index -> Observable<Int> in
guard index < 3 else { return Observable.error(error) }
return Observable.timer(.seconds(1), scheduler: MainScheduler.instance)
}
}
.subscribe(...)
.disposed(by: disposeBag)
七、框架核心优势
- 高度封装:业务层无需接触 Apollo 原生 API,一行代码发起请求,降低学习成本;
- 响应式编程:完全贴合 RxSwift/RxCocoa,天然支持请求取消、批量请求、状态绑定;
- 统一异常处理 :所有错误转换为自定义枚举,业务层只需处理
localizedDescription,无需区分原生错误类型; - 灵活配置:支持全局配置和单个请求自定义配置,覆盖所有业务场景;
- 易维护扩展:模块化拆分,新增拦截器 / 配置项只需修改核心层,不影响业务层;
- 生命周期安全 :请求自动绑定到
DisposeBag,页面销毁时自动取消请求,避免内存泄漏; - 兼容 Apollo 所有特性:保留 Apollo 的缓存、离线查询、批量操作等原生能力,不做阉割。
八、注意事项
- Apollo 代码生成 :必须确保 GraphQL 的
.graphql文件正确配置,Build Phases 的脚本能正常生成 Model 代码,否则框架无法使用; - Token 管理:建议将 Token 保存在 Keychain 中(而非 UserDefaults),提高安全性;
- 网络状态检查 :示例中的网络检查是简易版,建议使用
ReachabilitySwift库,支持蜂窝网络 / WiFi 的精准判断; - 线程切换 :框架已将结果切换到主线程,业务层可直接更新 UI,无需手动切换;
- 内存管理 :所有闭包中必须使用
[weak self],避免循环引用; - 调试日志 :
LogInterceptor仅在DEBUG模式下打印日志,避免生产环境泄露敏感信息; - Apollo 版本 :若使用 Apollo 0.x 版本,需微调
NetworkTransport和Interceptor的 API(1.0 + 有部分 API 变更)。
总结
这套框架基于Apollo + RxSwift/RxCocoa 实现了响应式、高复用、易维护的 GraphQL 网络层封装,核心亮点在于:
- 把 Apollo 原生回调无缝转换为 Rx 的
Single序列,贴合 iOS 响应式开发的主流思想; - 做了全局配置 + 单请求自定义的双层设计,兼顾通用性和灵活性;
- 统一了异常处理和响应结果,让业务层只需关注数据和 UI 绑定,大幅减少重复代码;
- 支持请求拦截、Token 全局管理、离线缓存等企业级开发的核心需求,可直接落地到生产项目。
后续可根据项目需求,扩展请求防抖 、全局错误弹窗 、请求埋点等功能,只需在核心拦截器或通用请求层添加逻辑即可,完全不影响现有业务代码。