Apollo iOS 1.0 迁移指南
从 0.x 迁移到 1.0
Apollo iOS 1.0 提供了一个稳定的 API,使用现代 Swift 语言约定和功能。
在众多改进中,Apollo iOS 1.0 新增了以下功能:
- 使用纯 Swift 代码编写的代码生成工具。
- 生成的模型提高了可读性和功能性,同时减少了生成的代码大小。
- 支持在多模块项目中使用生成的模型。
- 支持通过类型安全的 API 解析缓存键。
本文介绍了 Apollo iOS 这个主要版本的显著变化,以及从 Apollo iOS 0.x 迁移到 1.0 的步骤。
关键更改
生成的模式模块
0.x 版本的 Apollo iOS 为您的 GraphQL 操作定义、以及您模式中引用的输入对象和枚举生成模型。
Apollo iOS 1.0 在此基础上扩展,生成一个包含您的 GraphQL 模式及其类型定义的整个模块。
除了您的输入对象和枚举类型外,此模块还包含
- 您的模式中对象、接口和联合的类型。
- 可编辑的自定义标量定义。
- 可编辑
SchemaConfiguration.swift
文件。 - Apollo GraphQL执行器的元数据。
模式类型包含在其自己的命名空间中,以防止命名冲突。您可以生成此命名空间作为单独的模块,由您的项目导入,或作为您可以嵌入到应用程序目标中的非区分大小写的命名空间枚举。
多模块支持
Apollo iOS 1.0可以支持由多个模块组成的复杂应用程序或单体应用程序目标。
生成的模式模块支持多模块项目,因为它包含一个模式的所有共享类型和元数据。这使得您可以将生成的操作模型移动到项目结构中的任何位置(只要它们与模式模块相链接)。
Apollo iOS的代码生成引擎提供了灵活的配置选项,使得代码生成与任何项目结构无缝工作。
逐步说明
在迁移到Apollo iOS 1.0之前,您应考虑项目结构并决定如何包含您的生成的模式模块和操作模型。
要了解如何将Apollo iOS最佳集成到满足您的项目需求,请参阅我们的项目配置文档。
要迁移到Apollo iOS 1.0,您需要执行以下操作:
此迁移过程的大部分涉及新的代码生成机制。
在这个过程中,我们将解释如何逐步移除旧0.x版本的过时部分。以下每个步骤也包含了关于任何破坏性API更改的解释。
步骤 1:升级到Apollo iOS 1.0
首先,将您的Apollo iOS依赖项更新到最新版本。您可以使用Swift Package Manager (SPM)或Cocoapods将Apollo iOS作为一个包包含在内。
为了接收错误修复和新功能,我们建议包含从1.0
到下一个主要版本。
要查看Apollo iOS SDK提供的模块(并确定您需要哪些模块),请参阅SDK组件。
.package(url: "https://github.com/apollographql/apollo-ios.git",.upToNextMajor(from: "1.0.0")),
pod 'Apollo' ~> '1.0'
注意:您可以将 Apollo iOS 1.0 构建为动态 .xcframework
或静态库。您还可以使用 Carthage 或 Buck 等构建工具预编译并包含 Apollo iOS 1.0 的 二进制文件(尽管目前我们没有 文档 如何做这件事)。
第二步:设置代码生成
Apollo iOS 1.0 包含了一个新的代码生成引擎,该引擎是用纯 Swift 编码的,取代了旧的 apollo-tooling
库。要使用 1.0,您必须安装新的代码生成引擎并移除旧的引擎。
我们建议使用 Apollo Codegen CLI 运行新的代码生成引擎。您还可以在 Swift 脚本中运行代码生成以进行更高级的使用。
代码生成 CLI 设置
有关 CLI 设置说明,请选择您使用的方法来包含 Apollo
。
Swift脚本设置
如果您通过Swift脚本运行代码生成,请更新您的脚本以使用与您的Apollo版本匹配的ApolloCodgenLib
版本。
然后,在您的脚本中使用新的配置值更新ApolloCodegenConfiguration
。有关配置选项的列表,请参阅Codegen配置。
步骤3:替换代码生成构建阶段
我们不再建议将Apollo的代码生成作为Xcode构建阶段运行。
您生成的文件在您修改.graphql
操作定义(这种情况很少发生)时会发生更改。在每次构建上运行代码生成会增加构建时间并减缓开发速度。
相反,我们建议在您修改.graphql
文件时手动(使用CLI)运行代码生成。
如果您希望在每次构建时继续运行代码生成,您可以更新您的构建脚本来运行CLI的generate
命令。
步骤4:重构您的代码
在设计Apollo iOS 1.0的过程中,我们试图减少从旧版本迁移所需的代码更改数量。
以下是Apollo iOS 1.0带来的每个破坏性更改的解释以及如何在迁移过程中处理这些更改的提示。
破坏性更改
自定义标量
在Apollo iOS的0.x版本中,您模式的自定义标量默认情况下作为String
类型字段暴露。如果您使用了--passthroughCustomScalars
选项,则您生成的模型包括自定义标量的名称。您负责定义传递给自定义标量的类型。
在Apollo iOS 1.0中,操作模型使用自定义标量定义,默认情况下,Apollo iOS为所有引用的自定义标量生成typealias
定义。这些定义位于您的模式模块中。所有自定义标量的默认实现是一个typealias
到String
。
自定义标量文件一次生成。这意味着您可以编辑它们,并且后续的代码生成执行不会覆盖您的更改。
要将自定义标量类型迁移到Apollo iOS 1.0,请按照以下步骤操作:
- 将类型包含在您的模式模块中。
- 确保类型符合
CustomScalarType
协议。 - 将
typealias
定义指向新类型。- 或者,如果类型与您的自定义标量具有确切的名称,删除
typealias
定义。
- 或者,如果类型与您的自定义标量具有确切的名称,删除
有关定义自定义标量的更多详细信息,请参阅自定义标量。
示例
我们定义一个scalar Coordinate
,在GraphQL操作中进行引用。Apollo iOS生成Coordinate
自定义标量:
public extension MySchema {typealias Coordinate = String}
具有名称Coordinate
的自定义标量可以替换typealias
,如下所示:
public extension MySchema {struct Coordinate: CustomScalarType {let x: Intlet y: Intpublic init (_jsonValue value: JSONValue) throws {guard let value = value as? String,let coordinates = value.components(separatedBy: ",").compactMap({ Int($0) }),coordinates.count == 2 else {throw JSONDecodingError.couldNotConvert(value: value, to: Coordinate.self)}self.x = coordinates[0]self.y = coordinates[1]}public var _jsonValue: JSONValue {"\(x),\(y)"}}}
缓存键配置
在Apollo iOS 0.x版本中,您可以通过为ApolloClient
提供cacheKeyForObject
块来配置规范化缓存的缓存键计算。
在Apollo iOS 1.0中,我们用SchemaConfiguration.swift
文件中的类型安全API替换此功能,该文件由Apollo iOS与生成的模式类型一起生成。
要将缓存键配置代码进行迁移,将cacheKeyForObject
实现重构到SchemaConfiguration.swift
文件的cacheKeyInfo(for type:object:)
功能中。
在0.x中,我们建议您在缓存键中使用对象的__typename
前缀来防止键冲突。
Apollo iOS 1.0会自动执行此操作。如果您想根据不同类型的对象(例如,根据通用接口类型)对缓存键进行分组,您可以设置返回的CacheKeyInfo
的uniqueKeyGroup
属性。
有关新缓存键配置API的更多详细信息,请参阅自定义缓存键。
示例
给定一个cacheKeyForObject
块:
client.cacheKeyForObject = {guard let typename = $0["__typename"] as? String,let id = $0["id"] as? String else {return nil}return "\(typename):\(id)"}
您可以将此迁移到新的cacheKeyInfo(for type:object:)
函数,如下所示:
public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {guard let id = object["id"] as? String else {return nil}return CacheKeyInfo(id: id)}}
或者您可以使用JSON值便捷初始化器,如下所示:
public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {return try? CacheKeyInfo(jsonValue: object["id"])}}
本地缓存变异
在Apollo iOS的0.x版本中,您可以直接使用任何生成的操作或片段模型在本地缓存中更改数据。
直接缓存访问的API基本上保持不变,但生成的模型对象现在是默认不可变。您仍然可以使用生成的模型直接读取缓存数据,但为了变异缓存数据,您现在需要定义单独的本地缓存变异操作或片段。
您可以通过将@apollo_client_ios_localCacheMutation
指令应用于任何GraphQL操作或fragment定义来创建本地缓存变异模型。
有关新的本地缓存变异API的详细说明,请参阅直接缓存访问。
将缓存变异与网络操作分离
将一个查询标记为LocalCacheMutation
,该缓存变异的生成模型不再符合GraphQLQuery
。这意味着您不能再将其缓存变异用作查询操作。
从根本上讲,这是因为缓存变异模型是可变的,而网络响应数据是不可变的。缓存变异是为访问和修改必要数据而设计的。
如果我们的缓存变异模型是可变的,则在ReadWriteTransaction
之外变异它们不会将任何更改保存到缓存中。此外,可变数据模型需要生成的代码量几乎翻倍。通过保持不可变模型,我们避免了这种混淆并减少了我们的生成代码。
避免创建整个查询操作的可变版本。相反,定义可变异的片段或查询来变异所需的字段。
示例
给定一个操作和从Apollo iOS 0.x版本写入的交易:
query UserDetails {loggedInUser {idnameposts {idbody}}}
store.withinReadWriteTransaction({ transaction inlet cacheMutation = UserDetailsQuery()let newPost = UserDetailsQuery.Data.LoggedInUser.Post(id: "789, body: "This is a new post!")try transaction.update(cacheMutation) { (data: inout UserDetailsQuery.Data) indata.loggedInUser.posts.append(newPost)}})
在Apollo iOS 1.0中,您可以使用新的LocalCacheMutation
来重写这段代码:
query AddUserPostLocalCacheMutation @apollo_client_ios_localCacheMutation {loggedInUser {posts {idbody}}}
store.withinReadWriteTransaction({ transaction inlet cacheMutation = AddUserPostLocalCacheMutation()let newPost = AddUserPostLocalCacheMutation.Data.LoggedInUser.Post(data: DataDict(["__typename": "Post", "id": "789", "body": "This is a new post!"],variables: nil))try transaction.update(cacheMutation) { (data: inout AddUserPostLocalCacheMutation.Data) indata.loggedInUser.posts.append(newPost)}})
可空的输入值
根据GraphQL规范,显式地提供null
作为输入字段的值与未提供值(nil
)在语义上是不同的。
为了区分 null
和 nil
,Apollo iOS 0.x版本将可选输入值生成了双重可选值类型(??
,或 Optional<Optional<Value>>
)。这对于许多用户来说很困惑,并且没有清楚地表达API的意图。
在Apollo iOS 1.0中,我们用新的GraphQLNullable
包装枚举类型来替换双重可选值。
此新类型需要您明确指示输入字段的值或可空性行为。
虽然这个API稍微啰嗦一些,但它提供了清晰度,并减少了由意外行为引起的错误。
关于如何使用 GraphQLNullable
的更多示例和最佳实践,请参阅使用可空参数。
示例
如果我们向可空输入参数传递一个值,我们需要用 GraphQLNullable
包裹该值:
MyQuery(input: "Value")
MyQuery(input: .some("Value"))
要提供一个 null
或 nil
值,请分别使用 .null
或 .none
。
/// A `nil` double optional value translates to omission of the value.MyQuery(input: nil)/// An optional containing a `nil` value translates to an `null` value.MyQuery(input: .some(nil))
/// A `GraphQLNullable.none` value translates to omission of the value.MyQuery(input: .none)/// A `GraphQLNullable.null` value translates to an `null` value.MyQuery(input: .null)
当将可选值传递给可空输入值时,如果您的值是 nil
,则需要提供默认值:
var optionalInput: String? = nilMyQuery(input: optionalInput)
var optionalInput: String? = nilMyQuery(input: optionalInput ?? .null)
模拟操作模型进行测试
在 Apollo iOS 的 0.x 版本中,您可以通过使用每个模型生成的初始化器或直接使用 JSON 数据初始化它们来创建生成的操作模型的模拟。这两种方法都容易出错,步骤繁琐且脆弱。
Apollo iOS 1.0 提供了一种根据您的模式类型生成测试模拟的新方法。首先,将 output.testMocks
添加到您的代码生成配置中,然后将您的测试模拟链接到您的单元测试目标。
而不是使用类型的生成初始化器创建模型,您可以创建底层数据的方案类型的测试模拟。使用测试模拟,您可以设置相关 字段 的值,并初始化您的 操作模型。
Apollo iOS 1.0 的新测试模拟更易于理解且类型安全。它们还消除了为不同模型类型生成初始化器的需求。
请注意,您仍然可以使用 JSON 数据初始化您的操作模型,但初始化器已稍有变化。更多详细信息,请参阅 JSON 初始化器。
有关更多详细信息,请参阅 测试模拟。
示例
给定一个 Hero
接口类型,它可以是一个 Human
或 Droid
类型,以下为 operation 的定义:
query HeroDetails {hero {id... on Human {name}... on Droid {modelNumber}}}
Apollo iOS 的 0.x 版本为 HeroDetails.Data.Hero
模型上的每个类型生成初始化器:
struct Hero {static func makeHuman(id: String, name: String) {// ...}static func makeDroid(id: String, modelNumber: String) {// ...}}
这些初始化器在 Apollo iOS 1.0 中没有生成。相反,您可以直接初始化 Mock<Human>
或 Mock<Droid>
:
let mockHuman = Mock<Human>()mockHuman.id = "10"mockHuman.name = "Han Solo"let mockDroid = Mock<Droid>()mockDroid.id = "12"mockDroid.modelNumber = "R2-D2"
然后,使用您的测试模拟创建 HeroDetails.Data.Hero
模型的模拟:
let humanHero = HeroDetails.Data.Hero(from: mockHuman)let droidHero = HeroDetails.Data.Hero(from: mockDroid)
从 JSON 数据生成测试模拟
如果您希望继续直接使用 JSON 数据初始化模型,请更新初始化器以使用 init(data: DataDict)
初始化器创建您的模型。您还必须确保您的 JSON 数据为 [String: AnyHashable]
字典。
let json: [String: Any] = ["__typename: "Human",// ...]let hero = HeroDetails.Data.Hero(unsafeResultMap: json)
let json: [String: AnyHashable] = ["__typename: "Human",// ...]let hero = HeroDetails.Data.Hero(data: DataDict(json))