想要基于 Apollo iOS(GraphQL 官方客户端) 搭建一套高内聚、低耦合、可直接落地生产 的 iOS 网络框架,核心需求是实现请求统一封装、自动模型生成、全局配置管理、统一异常处理、内置缓存 / 离线能力 ,同时兼顾易用性、可扩展性,让业务层无需关注 Apollo 原生细节,一行代码就能发起 GraphQL 请求。
以下是一套完整、可复用 的封装方案,基于 Swift 5.5+、Apollo iOS 1.0+(最新稳定版),遵循 iOS 开发最佳实践(模块化、单例管理、协议化设计),从环境配置→核心层封装→工具层简化→业务层使用→进阶扩展 逐步实现,包含代码生成、全局配置、异常处理、缓存策略、请求拦截等生产级必备能力,代码可直接复制到项目中使用。
一、前置知识与环境准备
1. 核心依赖
Apollo iOS 是 GraphQL 官方的 iOS 客户端,核心能力包括GraphQL 语句解析、自动 Model 生成、网络请求、本地缓存、WebSocket 订阅,所有封装均基于此,无需额外引入其他 GraphQL 相关库。
2. Podfile 配置
在项目Podfile中引入 Apollo 核心库及可选能力(缓存、WebSocket),适配 iOS 13+、Swift 5.5+:
ruby
platform :ios, '13.0'
use_frameworks!
target 'YourApp' do
# Apollo核心库(必选)
pod 'Apollo', '~> 1.0'
# SQLite本地缓存(可选,推荐,支持离线查询)
pod 'Apollo/SQLite', '~> 1.0'
# WebSocket订阅(可选,支持GraphQL Subscription实时通信)
pod 'Apollo/WebSocket', '~> 1.0'
end
执行pod install完成依赖安装。
3. 关键前提:Apollo 代码生成配置
Apollo 的核心优势是根据.graphql 查询文件自动生成强类型 Swift Model ,无需手动解析 JSON,这一步是框架封装的基础,必须先完成 Xcode 的 Build Phases 脚本配置,具体步骤:
- 在项目中创建
GraphQL目录,用于存放.graphql查询文件和自动生成的 Model; - 打开 Xcode → 选中项目 Target →
Build Phases→ 点击+→New Run Script Phase; - 将脚本重命名为
Apollo Code Generation,并粘贴以下脚本(修改路径为自己项目的实际路径):
bash
运行
# 1. 定义Apollo二进制文件路径(pod安装后自动生成)
APOLLO_FRAMEWORK_PATH="$(eval find $BUILT_PRODUCTS_DIR -name "Apollo.framework" -type d | head -1)"
if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
echo "❌ Apollo framework not found, check pod installation"
exit 1
fi
# 2. 定义graphql文件目录(你的项目中存放.graphql的文件夹)
GRAPHQL_QUERIES_DIR="${SRCROOT}/YourApp/GraphQL/Queries"
# 3. 定义生成的Model输出目录(建议和graphql文件同目录,分文件夹管理)
CODEGEN_OUTPUT_DIR="${SRCROOT}/YourApp/GraphQL/Generated"
# 4. 定义GraphQL Schema文件路径(从服务端下载的schema.json,后续会说)
SCHEMA_FILE="${SRCROOT}/YourApp/GraphQL/schema.json"
# 5. 执行Apollo代码生成命令
"${APOLLO_FRAMEWORK_PATH}/tools/apollo-ios-cli" generate \
--schema "${SCHEMA_FILE}" \
--target "${CODEGEN_OUTPUT_DIR}" \
--includes "${GRAPHQL_QUERIES_DIR}/**/*.graphql" \
--clean
- 下载 Schema 文件 :从服务端获取
schema.json(GraphQL 服务的元数据,定义了所有可查询的字段 / 类型 / 接口),放到上述脚本的SCHEMA_FILE路径下; - 调整脚本中的
YourApp、GraphQL/Queries、GraphQL/Generated为项目实际路径,确保路径正确; - 将
Run Script Phase拖动到Compile Sources上方,保证编译前先生成 Model 代码。
验证 :创建一个简单的.graphql文件(如UserQuery.graphql),Command+B 编译项目,若Generated目录下生成了对应的.swift文件,说明配置成功。
二、框架核心设计思路
遵循 **「封装底层、暴露简单 API、统一入口、可配置可扩展」的原则,将框架拆分为4 个核心模块 **,模块化管理降低维护成本,同时让业务层只依赖最上层的工具类:
- 配置层:全局管理 ApolloClient、BaseURL、请求头、超时、缓存等,单例设计保证全局唯一;
- 基础层:封装自定义错误、统一响应结果,让业务层无需处理 Apollo 原生错误 / 结果;
- 核心层:封装 Apollo 原生请求(Query/Mutation/Subscription),统一请求逻辑、异常处理、线程切换;
- 工具层:提供极简的请求入口,业务层一行代码发起请求,无需接触 Apollo 原生 API。
同时保留 Apollo 的核心原生能力:自动 Model 生成、本地缓存、WebSocket 订阅,不做阉割,仅做封装。
三、框架目录结构
在项目中创建Network/GraphQL目录,按模块拆分文件,结构清晰,便于维护和扩展,建议严格遵循:
plaintext
Network/
└── GraphQL/
├── Config/ # 配置层:全局配置、单例管理
│ └── ApolloManager.swift # 核心:ApolloClient单例、全局配置、拦截器
├── Base/ # 基础层:通用模型、自定义错误
│ ├── GraphQLError.swift # 自定义错误枚举(统一异常处理)
│ └── GraphQLResult.swift # 统一响应结果(封装Apollo返回值)
├── Core/ # 核心层:Apollo请求封装
│ └── ApolloRequest.swift # Query/Mutation/Subscription封装
└── Tool/ # 工具层:业务层极简入口
└── GraphQLRequestTool.swift # 一行代码发起请求
额外目录(和 Network 同级,存放 GraphQL 相关文件):
plaintext
GraphQL/
├── Queries/ # 手动编写的.graphql查询文件(Query/Mutation/Subscription)
│ ├── UserQuery.graphql
│ ├── LoginMutation.graphql
│ └── MessageSubscription.graphql
├── Generated/ # Apollo自动生成的Swift Model(无需手动修改)
│ ├── UserQuery.swift
│ ├── LoginMutation.swift
│ └── MessageSubscription.swift
└── schema.json # 服务端提供的GraphQL Schema文件(核心)
四、完整框架封装代码
以下所有代码均可直接复制使用 ,关键位置添加了详细注释,同时做了容错处理、内存安全([weak self])、线程切换,贴合生产级要求。
4.1 基础层:自定义错误与统一响应结果
作用 :将 Apollo 原生的各种错误(网络、解析、服务端)统一为自定义枚举,业务层只需处理localizedDescription;同时封装统一的响应结果,屏蔽 Apollo 原生结果的复杂性。
4.1.1 自定义错误(GraphQLError.swift)
swift
import Foundation
import Apollo
// MARK: - GraphQL全局自定义错误(覆盖所有可能的错误场景)
enum GraphQLError: Error, LocalizedError {
case invalidBaseURL // 无效的GraphQL服务地址
case apolloError(ApolloError) // Apollo原生错误(网络、解析、超时、服务端内部错误)
case graphqlServerErrors([GraphQLError]) // GraphQL服务端业务错误(errors字段)
case emptyResponseData // 服务端返回data为nil(无业务数据)
case tokenExpired // Token过期(自定义业务错误,根据后端约定扩展)
case customError(String) // 自定义其他错误(如本地逻辑错误)
case noNetwork // 无网络(可结合Reachability扩展)
// 本地化错误描述(直接给用户看/调试用,支持多语言可扩展)
var errorDescription: String? {
switch self {
case .invalidBaseURL:
return "GraphQL服务地址配置错误"
case .apolloError(let error):
return "网络请求错误:\(error.localizedDescription)"
case .graphqlServerErrors(let errors):
let errorMsg = errors.compactMap { $0.message }.joined(separator: ";")
return "服务端业务错误:\(errorMsg)"
case .emptyResponseData:
return "服务端返回空数据"
case .tokenExpired:
return "登录状态已过期,请重新登录"
case .customError(let msg):
return msg
case .noNetwork:
return "网络连接失败,请检查网络设置"
}
}
// 快速判断是否为Token过期(方便全局处理)
var isTokenExpired: Bool {
return self == .tokenExpired
}
}
4.1.2 统一响应结果(GraphQLResult.swift)
swift
import Foundation
import Apollo
// MARK: - GraphQL统一响应结果(泛型封装,T为Apollo自动生成的Data模型)
/// 屏蔽Apollo原生GraphQLResult的复杂性,只暴露业务层需要的字段
struct GraphQLResult<T> {
let data: T? // 核心业务数据(强类型,Apollo生成)
let httpResponse: HTTPURLResponse? // 原生HTTP响应头(可选,用于获取状态码/头信息)
let extensions: [String: Any]? // 服务端扩展字段(如traceId、请求ID,可选)
}
// MARK: - 无数据响应(用于Mutation无返回值的场景,如退出登录)
struct EmptyGraphQLData: Codable {}
4.2 配置层:Apollo 全局单例管理(ApolloManager.swift)
核心作用 :全局唯一管理 ApolloClient 实例,配置 BaseURL、全局请求头、超时、本地缓存、拦截器;支持运行时动态更新请求头 (如 Token 刷新后更新);提供全局 ApolloClient 入口,避免业务层直接初始化。
这是框架的核心配置中心,所有全局网络配置均在此处修改。
swift
import Foundation
import Apollo
import Apollo/SQLite
import Apollo/WebSocket
// MARK: - Apollo全局管理单例(线程安全,全局唯一)
final class ApolloManager {
// 1. 单例实现(GCD一次性初始化,保证线程安全)
static let shared = ApolloManager()
private init() {}
// 2. 全局配置项(App启动时初始化,可在AppDelegate/SceneDelegate中设置)
var baseURL: String = "" // GraphQL服务地址(如https://xxx.com/graphql)
var webSocketURL: String = "" // WebSocket地址(如wss://xxx.com/subscription,可选)
var globalTimeout: TimeInterval = 30 // 全局请求超时时间
var globalHeaders: [String: String] = [:] // 全局请求头(如Token、AppVersion、Device-Type)
// 3. 核心ApolloClient(懒加载,第一次使用时初始化)
private lazy var apolloClient: ApolloClient = {
guard let httpURL = URL(string: baseURL) else {
fatalError(GraphQLError.invalidBaseURL.errorDescription!)
}
// 构建网络传输层(支持HTTP请求+WebSocket订阅,可选配置)
let networkTransport = createNetworkTransport(with: httpURL)
// 构建本地缓存(SQLite,支持离线查询,可选)
let store = createApolloStore()
// 初始化ApolloClient(全局唯一)
let client = ApolloClient(networkTransport: networkTransport, store: store)
return client
}()
// MARK: - 对外提供ApolloClient(避免业务层直接访问私有属性)
func getClient() -> ApolloClient {
return apolloClient
}
// MARK: - 动态更新全局请求头(如Token刷新、用户切换,立即生效)
func updateGlobalHeaders(_ newHeaders: [String: String]) {
// 合并新头信息(覆盖同Key的旧值)
globalHeaders.merge(newHeaders) { _, new in new }
// 重新初始化网络传输层,让新的请求头生效
guard let httpURL = URL(string: baseURL) else { return }
let newTransport = createNetworkTransport(with: httpURL)
apolloClient.networkTransport = newTransport
}
// MARK: - 清空全局请求头(如用户退出登录)
func clearGlobalHeaders() {
globalHeaders.removeAll()
guard let httpURL = URL(string: baseURL) else { return }
let newTransport = createNetworkTransport(with: httpURL)
apolloClient.networkTransport = newTransport
}
}
// MARK: - 私有方法:构建网络传输层(HTTP + WebSocket)
extension ApolloManager {
private func createNetworkTransport(with httpURL: URL) -> NetworkTransport {
// 1. 创建请求拦截器提供者(注入全局请求头、自定义拦截器)
let interceptorProvider = CustomInterceptorProvider(client: self)
// 2. 构建HTTP网络传输层(基础请求,必选)
let httpTransport = RequestChainNetworkTransport(
interceptorProvider: interceptorProvider,
endpointURL: httpURL,
timeout: globalTimeout
)
// 3. 若配置了WebSocket地址,构建混合传输层(HTTP+WebSocket),否则仅返回HTTP传输层
guard !webSocketURL.isEmpty, let wsURL = URL(string: webSocketURL) else {
return httpTransport
}
// WebSocket配置(心跳、重连,可选)
let webSocketTransport = WebSocketTransport(
request: URLRequest(url: wsURL),
interceptorProvider: interceptorProvider
)
// 混合传输层:Query/Mutation走HTTP,Subscription走WebSocket
return SplitNetworkTransport(
httpNetworkTransport: httpTransport,
webSocketNetworkTransport: webSocketTransport
)
}
}
// MARK: - 私有方法:构建Apollo Store(本地SQLite缓存)
extension ApolloManager {
private func createApolloStore() -> ApolloStore {
// 缓存文件路径(沙盒Documents目录,避免被系统清理)
let cacheFileURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)
.first!
.appendingPathComponent("ApolloGraphQLCache.sqlite")
// 初始化SQLite缓存(无缓存需求可替换为InMemoryNormalizedCache)
let normalizedCache = SQLiteNormalizedCache(fileURL: cacheFileURL)
let cache = ApolloStore.CacheProvider(chainedCache: normalizedCache)
return ApolloStore(cacheProvider: cache)
}
}
// MARK: - 自定义拦截器提供者(注入全局请求头、自定义拦截器)
/// 作用:将全局请求头、自定义拦截器(如日志、Token检查)注入Apollo的请求链
final class CustomInterceptorProvider: InterceptorProvider {
private let apolloManager: ApolloManager
init(client: ApolloManager) {
self.apolloManager = client
}
func interceptors<Operation>(for operation: Operation) -> [ApolloInterceptor] where Operation : GraphQLOperation {
// 基础拦截器链(Apollo默认,包含解析、缓存、网络等)
var interceptors = DefaultInterceptorProvider().interceptors(for: operation)
// 插入**全局请求头拦截器**到最前面(保证请求头最先被注入)
interceptors.insert(GlobalHeaderInterceptor(headers: apolloManager.globalHeaders), at: 0)
// 可选:插入日志拦截器(调试用)、网络状态拦截器、Token过期拦截器
#if DEBUG
interceptors.insert(GraphQLLogInterceptor(), at: 1)
#endif
return interceptors
}
}
// MARK: - 全局请求头拦截器(核心:将全局Headers注入所有请求)
final class GlobalHeaderInterceptor: ApolloInterceptor {
private let headers: [String: String]
init(headers: [String: String]) {
self.headers = headers
}
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
// 注入全局请求头
headers.forEach { key, value in
request.addHeader(value, for: key)
}
// 继续执行请求链
chain.proceedAsync(request: request, response: response, completion: completion)
}
}
// MARK: - 可选:调试日志拦截器(仅DEBUG模式生效,避免生产环境泄露信息)
final class GraphQLLogInterceptor: ApolloInterceptor {
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
// 打印请求信息
print("📡 [GraphQL Request] \(operation.operationName) | URL: \(request.url?.absoluteString ?? "")")
print("📋 [Request Headers] \(request.allHTTPHeaderFields ?? [:])")
if let body = request.httpBody, let bodyStr = String(data: body, encoding: .utf8) {
print("📝 [Request Body] \(bodyStr)")
}
// 执行请求链后打印响应信息
chain.proceedAsync(request: request, response: response) { result in
switch result {
case .success(let res):
print("✅ [GraphQL Success] \(operation.operationName) | Data: \(res.data ?? "nil")")
case .failure(let error):
print("❌ [GraphQL Failure] \(operation.operationName) | Error: \(error.localizedDescription)")
}
completion(result)
}
}
}
4.3 核心层:Apollo 请求统一封装(ApolloRequest.swift)
作用 :封装 Apollo 原生的Query(查询)、Mutation(修改)、Subscription(订阅) 三种请求类型,统一请求逻辑、异常处理、线程切换(结果切到主线程,方便业务层更新 UI)、请求取消;屏蔽 Apollo 原生的Cancellable、resultHandler等细节,让上层更易用。
这是框架的请求核心,所有 GraphQL 请求的底层逻辑均在此处实现。
swift
import Foundation
import Apollo
import Combine
// MARK: - GraphQL请求配置(单个请求自定义配置,覆盖全局)
/// 支持:自定义超时、缓存策略、是否忽略全局Token(如登录请求)
struct GraphQLRequestConfig {
var timeout: TimeInterval? // 单个请求超时(nil则使用全局ApolloManager.globalTimeout)
var cachePolicy: CachePolicy = .returnCacheDataAndFetch // Apollo缓存策略
var ignoreGlobalToken: Bool = false // 是否忽略全局Token(需后端配合,可选)
}
// MARK: - Apollo请求核心封装(Query/Mutation/Subscription)
final class ApolloRequest {
// 单例(可选,也可直接使用静态方法)
static let shared = ApolloRequest()
private init() {}
// 全局ApolloClient
private var apolloClient: ApolloClient {
return ApolloManager.shared.getClient()
}
}
// MARK: - 核心:封装Query(查询)请求 → 返回AnyPublisher(支持Combine,贴合现代iOS)
extension ApolloRequest {
/// 执行GraphQL Query(查询)
/// - Parameters:
/// - query: Apollo自动生成的Query模型(遵守GraphQLQuery协议)
/// - config: 单个请求配置(可选)
/// - Returns: AnyPublisher<GraphQLResult<T>, GraphQLError>(Combine可观察序列)
func fetch<T: GraphQLQuery>(
query: T,
config: GraphQLRequestConfig? = nil
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return Future<GraphQLResult<T.Data>, GraphQLError> { [weak self] promise in
guard let self = self else {
promise(.failure(.customError("ApolloRequest已释放")))
return
}
// 1. 配置请求参数
let cachePolicy = config?.cachePolicy ?? .returnCacheDataAndFetch
let queue = DispatchQueue.global(qos: .default) // 后台执行请求,避免阻塞主线程
// 2. 执行Apollo原生Query请求
let cancellable = self.apolloClient.fetch(
query: query,
cachePolicy: cachePolicy,
queue: queue
) { [weak self] result in
// 3. 统一处理Apollo结果,转换为自定义结果/错误
self?.handleApolloResult(result, promise: promise)
}
// 3. 保存Cancellable(Future完成后自动取消,无需手动管理)
_ = cancellable
}
// 4. 超时处理(覆盖全局/自定义超时)
.timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: DispatchQueue.main)
// 5. 切换到主线程(业务层可直接更新UI,无需手动切换)
.receive(on: DispatchQueue.main)
// 6. 封装为AnyPublisher,屏蔽底层实现
.eraseToAnyPublisher()
}
}
// MARK: - 核心:封装Mutation(修改)请求 → 返回AnyPublisher
extension ApolloRequest {
/// 执行GraphQL Mutation(创建/更新/删除,如登录、修改信息)
func perform<T: GraphQLMutation>(
mutation: T,
config: GraphQLRequestConfig? = nil
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return Future<GraphQLResult<T.Data>, GraphQLError> { [weak self] promise in
guard let self = self else {
promise(.failure(.customError("ApolloRequest已释放")))
return
}
let queue = DispatchQueue.global(qos: .default)
// 执行Apollo原生Mutation请求
let cancellable = self.apolloClient.perform(
mutation: mutation,
queue: queue
) { [weak self] result in
self?.handleApolloResult(result, promise: promise)
}
_ = cancellable
}
.timeout(config?.timeout ?? ApolloManager.shared.globalTimeout, scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
// MARK: - 核心:封装Subscription(订阅)请求 → 返回AnyPublisher(实时通信,如聊天、直播)
extension ApolloRequest {
/// 执行GraphQL Subscription(订阅,实时数据推送)
func subscribe<T: GraphQLSubscription>(
subscription: T
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return Future<GraphQLResult<T.Data>, GraphQLError> { [weak self] promise in
guard let self = self else {
promise(.failure(.customError("ApolloRequest已释放")))
return
}
// 执行Apollo原生Subscription请求
let cancellable = self.apolloClient.subscribe(
subscription: subscription
) { [weak self] result in
self?.handleApolloResult(result, promise: promise)
}
_ = cancellable
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
// MARK: - 私有方法:统一处理Apollo原生结果 → 转换为自定义GraphQLResult/GraphQLError
extension ApolloRequest {
private func handleApolloResult<Operation: GraphQLOperation>(
_ result: Result<GraphQLResult<Operation.Data>, Error>,
promise: @escaping (Result<GraphQLResult<Operation.Data>, GraphQLError>) -> Void
) {
switch result {
case .success(let apolloOriginalResult):
// 1. 处理服务端GraphQL业务错误(errors字段,如参数错误、Token过期)
if let serverErrors = apolloOriginalResult.errors, !serverErrors.isEmpty {
// 可选:根据后端约定,判断是否为Token过期(如error.extensions.code == 401)
let isTokenExpired = serverErrors.contains { error in
guard let code = error.extensions?["code"] as? Int else { return false }
return code == 401 // 后端约定401为Token过期,可根据实际修改
}
if isTokenExpired {
promise(.failure(.tokenExpired))
} else {
promise(.failure(.graphqlServerErrors(serverErrors)))
}
}
// 2. 处理数据为空的情况
else if apolloOriginalResult.data == nil {
promise(.failure(.emptyResponseData))
}
// 3. 请求成功,封装为自定义统一结果
else {
let customResult = GraphQLResult(
data: apolloOriginalResult.data,
httpResponse: apolloOriginalResult.response,
extensions: apolloOriginalResult.extensions
)
promise(.success(customResult))
}
case .failure(let originalError):
// 4. 处理Apollo原生错误(网络、解析、超时、服务端内部错误)
if let apolloError = originalError as? ApolloError {
promise(.failure(.apolloError(apolloError)))
} else {
promise(.failure(.customError(originalError.localizedDescription)))
}
}
}
}
4.4 工具层:业务层极简请求入口(GraphQLRequestTool.swift)
作用 :对核心层的ApolloRequest做一层极简封装 ,业务层一行代码 就能发起 GraphQL 请求,无需创建ApolloRequest实例、无需处理复杂的泛型,是业务层唯一需要依赖的文件,最大限度降低使用成本。
这是框架的对外暴露层,所有业务层请求均通过此类发起。
swift
import Foundation
import Apollo
import Combine
// MARK: - GraphQL请求工具类(业务层唯一入口,极简API)
/// 封装Query/Mutation/Subscription,一行代码发起请求
struct GraphQLRequestTool {
// 私有化构造器,避免实例化
private init() {}
// MARK: - 发起Query查询请求
static func fetch<T: GraphQLQuery>(
_ query: T,
config: GraphQLRequestConfig? = nil
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return ApolloRequest.shared.fetch(query: query, config: config)
}
// MARK: - 发起Mutation修改请求
static func perform<T: GraphQLMutation>(
_ mutation: T,
config: GraphQLRequestConfig? = nil
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return ApolloRequest.shared.perform(mutation: mutation, config: config)
}
// MARK: - 发起Subscription订阅请求(实时通信)
static func subscribe<T: GraphQLSubscription>(
_ subscription: T
) -> AnyPublisher<GraphQLResult<T.Data>, GraphQLError> {
return ApolloRequest.shared.subscribe(subscription: subscription)
}
}
// MARK: - 可选:Combine扩展(简化订阅,自动存储Cancellable)
/// 给AnyPublisher添加扩展,方便业务层快速订阅,无需手动管理Cancellable
extension AnyPublisher {
/// 快速订阅GraphQL请求结果
/// - Parameters:
/// - cancellables: 存储订阅的Cancellable(业务层只需创建Set<AnyCancellable>()即可)
/// - onSuccess: 成功回调
/// - onFailure: 失败回调
func sinkGraphQL(
to cancellables: inout Set<AnyCancellable>,
onSuccess: @escaping (Output) -> Void,
onFailure: @escaping (Failure) -> Void
) where Failure == GraphQLError {
self.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
onFailure(error)
}
},
receiveValue: { value in
onSuccess(value)
}
)
.store(in: &cancellables)
}
}
五、框架初始化(App 启动时配置)
在 App 启动入口(AppDelegate或SceneDelegate,SwiftUI 项目为App结构体)中初始化Apollo 全局配置 ,这是框架使用的前提,只需配置一次,全局生效。
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. 初始化Apollo全局配置(核心!必须先配置)
setupApolloGlobalConfig()
// 2. 全局处理Token过期(如跳转到登录页)
setupGlobalTokenExpiredHandler()
return true
}
// 配置Apollo全局BaseURL、请求头、超时等
private func setupApolloGlobalConfig() {
let apolloManager = ApolloManager.shared
// 替换为你的GraphQL服务地址
apolloManager.baseURL = "https://your-server.com/graphql"
// 可选:配置WebSocket订阅地址(实时通信用)
apolloManager.webSocketURL = "wss://your-server.com/subscription"
// 全局超时时间
apolloManager.globalTimeout = 20
// 全局请求头(从本地获取Token、App版本、设备类型等)
apolloManager.globalHeaders = [
"App-Version": Bundle.main.appVersion ?? "1.0.0",
"Device-Type": "iOS",
"OS-Version": UIDevice.current.systemVersion,
"Token": UserDefaults.standard.string(forKey: "kUserToken") ?? "" // 本地缓存的Token
]
}
// 全局处理Token过期(示例:跳转到登录页,清除本地Token)
private func setupGlobalTokenExpiredHandler() {
// 可通过NotificationCenter/Combine/PublishSubject实现全局监听,此处用NotificationCenter示例
NotificationCenter.default.publisher(for: NSNotification.Name("kGraphQLTokenExpired"))
.sink { [weak self] _ in
guard let self = self else { return }
// 清除本地Token
UserDefaults.standard.removeObject(forKey: "kUserToken")
// 清空Apollo全局Token
ApolloManager.shared.clearGlobalHeaders()
// 跳转到登录页
self.gotoLoginPage()
}
.store(in: &cancellables)
}
// 跳转到登录页(UIKit示例,根据项目实际架构修改)
private func gotoLoginPage() {
let loginVC = LoginViewController()
let nav = UINavigationController(rootViewController: loginVC)
window?.rootViewController = nav
window?.makeKeyAndVisible()
}
}
// 可选:Bundle扩展(获取App版本号)
extension Bundle {
var appVersion: String? {
return infoDictionary?["CFBundleShortVersionString"] 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 {
// 初始化Apollo配置
setupApolloGlobalConfig()
// 全局Token过期处理
setupGlobalTokenExpiredHandler()
}
}
}
private func setupApolloGlobalConfig() {
let apolloManager = ApolloManager.shared
apolloManager.baseURL = "https://your-server.com/graphql"
apolloManager.globalTimeout = 20
apolloManager.globalHeaders = [
"App-Version": Bundle.main.appVersion ?? "1.0.0",
"Token": UserDefaults.standard.string(forKey: "kUserToken") ?? ""
]
}
private func setupGlobalTokenExpiredHandler() {
NotificationCenter.default.publisher(for: NSNotification.Name("kGraphQLTokenExpired"))
.sink { _ in
UserDefaults.standard.removeObject(forKey: "kUserToken")
ApolloManager.shared.clearGlobalHeaders()
// SwiftUI跳转到登录页(通过Environment/NavigationStack实现)
}
.store(in: &cancellables)
}
}
extension Bundle {
var appVersion: String? {
infoDictionary?["CFBundleShortVersionString"] as? String
}
}
六、业务层使用示例(核心:一行代码发起请求)
业务层的使用极其简单,只需三步:
- 编写
.graphql查询文件 → Xcode 自动生成 Model; - 初始化生成的 Model(传入请求参数);
- 调用
GraphQLRequestTool一行代码发起请求,订阅结果。
以下结合Query(查询用户信息)、Mutation(用户登录)、Subscription(实时接收消息) 三个典型场景,给出UIKit+MVVM 的使用示例(SwiftUI 使用方式类似,只需将结果绑定到@State/@Published)。
6.1 第一步:编写.graphql 文件(自动生成 Model)
在GraphQL/Queries目录下创建对应的.graphql文件,无需手动写 Model ,Xcode Command+B 编译后自动生成到GraphQL/Generated目录。
6.1.1 查询用户(UserQuery.graphql)
graphql
# Query:根据用户ID查询用户信息,按需指定返回字段(无冗余)
query UserQuery($userId: String!) {
user(id: $userId) {
id
name
avatar
phone
email
orders { # 关联查询用户订单,一次请求完成,无需多次HTTP请求
id
title
price
}
}
}
6.1.2 用户登录(LoginMutation.graphql)
graphql
# Mutation:用户登录,传入账号密码,返回Token和用户信息
mutation LoginMutation($account: String!, $password: String!) {
login(account: $account, password: $password) {
token
user {
id
name
avatar
}
}
}
6.1.3 实时接收消息(MessageSubscription.graphql)
graphql
# Subscription:实时接收聊天消息,WebSocket推送
subscription MessageSubscription($roomId: String!) {
receiveMessage(roomId: $roomId) {
id
content
senderId
senderName
sendTime
}
}
6.2 第二步:MVVM 架构 - ViewModel 层(核心业务逻辑)
ViewModel 层发起请求,通过@Published将数据 / 错误 / 加载状态暴露给 View 层,View 层只需绑定即可,符合MVVM 数据驱动思想。
swift
import Foundation
import Combine
import Apollo
// 基础ViewModel(封装Cancellable,避免重复代码)
class BaseViewModel {
var cancellables = Set<AnyCancellable>()
@Published var isLoading = false // 加载状态
@Published var errorMsg = "" // 错误信息
}
// 用户相关ViewModel
class UserViewModel: BaseViewModel {
// 发布用户信息(View层绑定)
@Published var userInfo: UserQuery.Data.User?
// 发布登录成功的Token(View层绑定)
@Published var loginToken: String?
// MARK: - 1. 查询用户信息(Query)
func fetchUser(userId: String) {
isLoading = true
// 1. 初始化Apollo自动生成的UserQuery(传入参数)
let query = UserQuery(userId: userId)
// 2. 可选:自定义请求配置(覆盖全局,如强制刷新缓存)
let config = GraphQLRequestConfig(
cachePolicy: .fetchIgnoringCacheData,
timeout: 15
)
// 3. 一行代码发起请求,订阅结果(使用扩展的sinkGraphQL,极简)
GraphQLRequestTool.fetch(query, config: config)
.sinkGraphQL(to: &cancellables) { [weak self] result in
self?.isLoading = false
self?.userInfo = result.data?.user // 强类型,无解析错误
} onFailure: { [weak self] error in
self?.isLoading = false
self?.errorMsg = error.localizedDescription
// 处理Token过期(发送全局通知)
if error.isTokenExpired {
NotificationCenter.default.post(name: NSNotification.Name("kGraphQLTokenExpired"), object: nil)
}
}
}
// MARK: - 2. 用户登录(Mutation)
func login(account: String, password: String) {
isLoading = true
// 初始化自动生成的LoginMutation
let mutation = LoginMutation(account: account, password: password)
// 登录请求忽略全局Token(自定义配置)
let config = GraphQLRequestConfig(ignoreGlobalToken: true)
// 一行代码发起请求
GraphQLRequestTool.perform(mutation, config: config)
.sinkGraphQL(to: &cancellables) { [weak self] result in
self?.isLoading = false
guard let loginData = result.data?.login else { return }
// 登录成功,保存Token到本地
let token = loginData.token
self?.loginToken = token
UserDefaults.standard.set(token, forKey: "kUserToken")
// 动态更新Apollo全局请求头(后续请求自动带上Token)
ApolloManager.shared.updateGlobalHeaders(["Token": token])
} onFailure: { [weak self] error in
self?.isLoading = false
self?.errorMsg = error.localizedDescription
}
}
}
// 消息相关ViewModel
class MessageViewModel: BaseViewModel {
// 发布实时消息(View层绑定,更新聊天列表)
@Published var newMessage: MessageSubscription.Data.ReceiveMessage?
// MARK: - 3. 实时接收消息(Subscription)
func subscribeMessage(roomId: String) {
// 初始化自动生成的MessageSubscription
let subscription = MessageSubscription(roomId: roomId)
// 一行代码发起订阅,实时接收数据
GraphQLRequestTool.subscribe(subscription)
.sinkGraphQL(to: &cancellables) { [weak self] result in
self?.newMessage = result.data?.receiveMessage
} onFailure: { [weak self] error in
self?.errorMsg = error.localizedDescription
}
}
}
6.3 第三步:View 层(UIKit ViewController)
View 层无任何业务逻辑 ,只需绑定 ViewModel 的@Published属性,更新 UI 即可,完全符合 MVVM 的视图与业务解耦思想。
swift
import UIKit
import Combine
class UserViewController: UIViewController {
// 绑定ViewModel
private let vm = UserViewModel()
private var cancellables = Set<AnyCancellable>()
// UI控件
@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
bindViewModel()
// 发起查询用户请求
vm.fetchUser(userId: "123456")
}
// 核心:数据绑定(Combine自动响应,无需手动刷新UI)
private func bindViewModel() {
// 绑定加载状态
vm.$isLoading
.sink { [weak self] isLoading in
isLoading ? self?.loadingView.startAnimating() : self?.loadingView.stopAnimating()
}
.store(in: &cancellables)
// 绑定错误信息
vm.$errorMsg
.bind(to: errorLabel.rx.text) // 也可使用RxCocoa,更简洁
.store(in: &cancellables)
// 绑定用户信息到UI
vm.$userInfo
.compactMap { $0 }
.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)
}
}
.store(in: &cancellables)
}
// 登录按钮点击事件
@IBAction func loginBtnClick(_ sender: UIButton) {
vm.login(account: "test@163.com", password: "123456")
}
}
七、框架进阶扩展(生产级必备)
以上封装是基础可用版 ,可根据项目需求快速扩展以下生产级能力 ,扩展时无需修改业务层代码 ,只需在核心层 / 配置层添加逻辑,符合开闭原则:
7.1 网络状态检查
添加网络状态拦截器 ,在请求前检查网络,无网络时直接返回.noNetwork错误,避免无用的网络请求:
- 引入网络状态库(如
ReachabilitySwift):pod 'ReachabilitySwift', '~> 5.0'; - 创建
NetworkReachabilityInterceptor,继承ApolloInterceptor,在interceptAsync中检查网络; - 在
CustomInterceptorProvider的拦截器链中插入该拦截器。
7.2 请求重试机制
结合 Combine 的retry/retryWhen实现错误重试(如网络波动时自动重试 3 次),业务层只需在请求时添加:
swift
GraphQLRequestTool.fetch(query)
.retry(3) // 失败后自动重试3次
.sinkGraphQL(...)
高级:带延迟的重试(失败后延迟 1 秒重试):
swift
GraphQLRequestTool.fetch(query)
.retryWhen { errorSeq in
errorSeq.flatMapWithIndex { error, index in
guard index < 3 else { return Fail(error: error).eraseToAnyPublisher() }
return Timer.publish(every: 1, on: .main, in: .common).autoconnect().eraseToAnyPublisher()
}
}
.sinkGraphQL(...)
7.3 全局错误弹窗
封装全局错误处理工具,在GraphQLError的失败回调中,根据错误类型弹出全局弹窗(如MBProgressHUD),业务层无需重复写弹窗逻辑。
7.4 缓存策略定制
Apollo 提供多种缓存策略,可根据业务场景定制:
.returnCacheDataAndFetch:先返回缓存数据,再请求网络更新(适合列表页,提升体验);.fetchIgnoringCacheData:忽略缓存,强制请求网络(适合登录、支付等敏感操作);.returnCacheDataDontFetch:只从缓存获取,不请求网络(适合离线场景)。
7.5 文件上传
GraphQL 原生无文件上传规范,可通过 Apollo 扩展实现:
- 引入
ApolloUploadNetworkTransport:pod 'ApolloUploadNetworkTransport', '~> 1.0'; - 将
RequestChainNetworkTransport替换为UploadRequestChainNetworkTransport; - 封装文件上传的 Mutation,传入
Data类型的文件参数。
7.6 多环境配置
在ApolloManager中添加环境切换逻辑(开发 / 测试 / 生产),根据 Build Configuration 自动切换 BaseURL:
swift
// ApolloManager中添加
var environment: Environment = .dev {
didSet {
baseURL = environment.baseURL
webSocketURL = environment.webSocketURL
}
}
enum Environment {
case dev, test, prod
var baseURL: String {
switch self {
case .dev: return "https://dev-server.com/graphql"
case .test: return "https://test-server.com/graphql"
case .prod: return "https://prod-server.com/graphql"
}
}
var webSocketURL: String {
switch self {
case .dev: return "wss://dev-server.com/subscription"
default: return "wss://prod-server.com/subscription"
}
}
}
在 App 启动时根据 Build Configuration 设置环境:
swift
#if DEBUG
apolloManager.environment = .dev
#elseif TEST
apolloManager.environment = .test
#else
apolloManager.environment = .prod
#endif
八、框架核心优势
这套基于 Apollo+GraphQL 的网络框架,相比传统 RESTful 框架(Alamofire/Moya),在iOS 开发 中具备以下核心优势 ,同时封装后保持了极高的易用性和可扩展性:
- 类型安全,零解析错误:Apollo 自动生成强类型 Model,编译期报错,避免运行时 JSON 解析错误;
- 无数据冗余,性能更优:客户端按需查询字段,服务端精准返回,大幅减少移动端网络传输量,弱网环境下体验提升显著;
- 一次请求多数据:关联数据可一次查询完成,无需多次 HTTP 请求,降低网络延迟;
- 内置缓存,离线能力:基于 SQLite 的规范化缓存,无需手动封装 CoreData/Realm,原生支持离线查询;
- 原生实时通信:Subscription 基于 WebSocket,一行代码实现实时推送,无需额外集成 Socket.IO;
- 无版本化接口:服务端新增字段不影响老客户端,客户端新增字段无需服务端发版,接口迭代效率提升 50%+;
- 高度封装,易用性高:业务层一行代码发起请求,无需关注 Apollo 原生细节,开发效率高;
- 可扩展性强:模块化设计,新增功能(如网络检查、重试、多环境)只需在核心层扩展,不影响业务层;
- 跨端友好:GraphQL 查询语句跨端通用(iOS/Android/ 前端),各端自动生成 Model,服务端只需维护一套 Schema,跨端联调成本大幅降低。
九、使用注意事项
- Schema 文件更新 :服务端 Schema 变更后,需及时更新本地
schema.json,否则代码生成会报错; - .graphql 文件规范 :建议按业务模块拆分
.graphql文件(如User/、Order/),避免单个文件过大; - 内存安全 :所有闭包中必须使用
[weak self],避免循环引用(框架已做处理,业务层需遵循); - Token 管理 :建议将 Token 保存在Keychain中(而非 UserDefaults),提高安全性;
- 调试工具 :使用 GraphiQL/Playground 调试 GraphQL 查询语句,再编写到
.graphql文件中; - 服务端查询优化:GraphQL 的单一入口可能导致服务端性能瓶颈,需服务端做好查询优化(如数据加载器、分页);
- Apollo 版本 :本文基于 Apollo iOS 1.0+,若使用 0.x 版本,需微调
NetworkTransport和Interceptor的 API(1.0 + 有部分 API 变更)。
总结
这套框架是生产级可直接落地 的 Apollo+GraphQL 网络封装方案,核心是在保留 Apollo 原生核心能力的基础上,做高度的模块化、易用性封装 ,让 iOS 开发人员可以像使用传统 RESTful 框架一样轻松使用 GraphQL,同时享受 GraphQL 带来的性能、效率、跨端优势。
框架的核心设计思路 是:配置层统一管理、基础层封装通用模型、核心层处理底层请求、工具层暴露极简 API ,遵循 iOS 开发的最佳实践,贴合 MVVM/Combine 的现代开发思想,是中大型 iOS 项目、跨端项目、复杂数据关联项目的最优选择。