使用Apollo和GraphQL搭建一套网络框架

想要基于 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 脚本配置,具体步骤:

  1. 在项目中创建GraphQL目录,用于存放.graphql查询文件和自动生成的 Model;
  2. 打开 Xcode → 选中项目 Target → Build Phases → 点击+New Run Script Phase
  3. 将脚本重命名为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
  1. 下载 Schema 文件 :从服务端获取schema.json(GraphQL 服务的元数据,定义了所有可查询的字段 / 类型 / 接口),放到上述脚本的SCHEMA_FILE路径下;
  2. 调整脚本中的YourAppGraphQL/QueriesGraphQL/Generated项目实际路径,确保路径正确;
  3. Run Script Phase拖动到Compile Sources上方,保证编译前先生成 Model 代码。

验证 :创建一个简单的.graphql文件(如UserQuery.graphql),Command+B 编译项目,若Generated目录下生成了对应的.swift文件,说明配置成功。

二、框架核心设计思路

遵循 **「封装底层、暴露简单 API、统一入口、可配置可扩展」的原则,将框架拆分为4 个核心模块 **,模块化管理降低维护成本,同时让业务层只依赖最上层的工具类:

  1. 配置层:全局管理 ApolloClient、BaseURL、请求头、超时、缓存等,单例设计保证全局唯一;
  2. 基础层:封装自定义错误、统一响应结果,让业务层无需处理 Apollo 原生错误 / 结果;
  3. 核心层:封装 Apollo 原生请求(Query/Mutation/Subscription),统一请求逻辑、异常处理、线程切换;
  4. 工具层:提供极简的请求入口,业务层一行代码发起请求,无需接触 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 原生的CancellableresultHandler等细节,让上层更易用。

这是框架的请求核心,所有 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 启动入口(AppDelegateSceneDelegate,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
    }
}

六、业务层使用示例(核心:一行代码发起请求)

业务层的使用极其简单,只需三步:

  1. 编写.graphql查询文件 → Xcode 自动生成 Model;
  2. 初始化生成的 Model(传入请求参数);
  3. 调用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错误,避免无用的网络请求:

  1. 引入网络状态库(如ReachabilitySwift):pod 'ReachabilitySwift', '~> 5.0'
  2. 创建NetworkReachabilityInterceptor,继承ApolloInterceptor,在interceptAsync中检查网络;
  3. 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 扩展实现:

  1. 引入ApolloUploadNetworkTransportpod 'ApolloUploadNetworkTransport', '~> 1.0'
  2. RequestChainNetworkTransport替换为UploadRequestChainNetworkTransport
  3. 封装文件上传的 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 开发 中具备以下核心优势 ,同时封装后保持了极高的易用性和可扩展性

  1. 类型安全,零解析错误:Apollo 自动生成强类型 Model,编译期报错,避免运行时 JSON 解析错误;
  2. 无数据冗余,性能更优:客户端按需查询字段,服务端精准返回,大幅减少移动端网络传输量,弱网环境下体验提升显著;
  3. 一次请求多数据:关联数据可一次查询完成,无需多次 HTTP 请求,降低网络延迟;
  4. 内置缓存,离线能力:基于 SQLite 的规范化缓存,无需手动封装 CoreData/Realm,原生支持离线查询;
  5. 原生实时通信:Subscription 基于 WebSocket,一行代码实现实时推送,无需额外集成 Socket.IO
  6. 无版本化接口:服务端新增字段不影响老客户端,客户端新增字段无需服务端发版,接口迭代效率提升 50%+;
  7. 高度封装,易用性高:业务层一行代码发起请求,无需关注 Apollo 原生细节,开发效率高;
  8. 可扩展性强:模块化设计,新增功能(如网络检查、重试、多环境)只需在核心层扩展,不影响业务层;
  9. 跨端友好:GraphQL 查询语句跨端通用(iOS/Android/ 前端),各端自动生成 Model,服务端只需维护一套 Schema,跨端联调成本大幅降低。

九、使用注意事项

  1. Schema 文件更新 :服务端 Schema 变更后,需及时更新本地schema.json,否则代码生成会报错;
  2. .graphql 文件规范 :建议按业务模块拆分.graphql文件(如User/Order/),避免单个文件过大;
  3. 内存安全 :所有闭包中必须使用[weak self],避免循环引用(框架已做处理,业务层需遵循);
  4. Token 管理 :建议将 Token 保存在Keychain中(而非 UserDefaults),提高安全性;
  5. 调试工具 :使用 GraphiQL/Playground 调试 GraphQL 查询语句,再编写到.graphql文件中;
  6. 服务端查询优化:GraphQL 的单一入口可能导致服务端性能瓶颈,需服务端做好查询优化(如数据加载器、分页);
  7. Apollo 版本 :本文基于 Apollo iOS 1.0+,若使用 0.x 版本,需微调NetworkTransportInterceptor的 API(1.0 + 有部分 API 变更)。

总结

这套框架是生产级可直接落地 的 Apollo+GraphQL 网络封装方案,核心是在保留 Apollo 原生核心能力的基础上,做高度的模块化、易用性封装 ,让 iOS 开发人员可以像使用传统 RESTful 框架一样轻松使用 GraphQL,同时享受 GraphQL 带来的性能、效率、跨端优势。

框架的核心设计思路 是:配置层统一管理、基础层封装通用模型、核心层处理底层请求、工具层暴露极简 API ,遵循 iOS 开发的最佳实践,贴合 MVVM/Combine 的现代开发思想,是中大型 iOS 项目、跨端项目、复杂数据关联项目的最优选择

相关推荐
终端安全笔记5 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程5 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne11 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008891 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
SoaringHeart2 天前
Swift 进阶&封装:仿 Get 给SwiftUI 添加路由导航能力
swiftui·swift
用户38034165882972 天前
Broadcast Extension 里的 Live Activity:一次跨进程妥协
ios·音视频开发