Apollo iOS 1.0 migration guide
From 0.x to 1.0
Apollo iOS 1.0 provides a stable API that uses modern Swift language conventions and features.
Among other improvements, Apollo iOS 1.0 features new:
-
Code generation tooling written in pure Swift code.
Generated models that improve readability and functionality while reducing generated code size.
Support for using generated models in multi-module projects.
Type-safe APIs for cache key resolution.
This article describes the significant changes for this major version of Apollo iOS and a walk-through of migrating an existing project from Apollo iOS 0.x to 1.0.
Steps to migrate to Apollo iOS 1.0:
Begin by reading about the key changes.
Next, follow our step-by-step instructions to upgrade your existing app.
Finally, see breaking changes to resolve any remaining build errors.
Key changes
Generated schema module
The 0.x version of Apollo iOS generates models for your GraphQL operation definitions and the input objects and enums referenced in your schema.
Apollo iOS 1.0 expands on this, generating an entire module that contains models and metadata for your GraphQL schema and its type definitions.
In addition to your input objects and enum types, this module contains:
Types for the objects, interfaces, and unions in your schema.
Editable definitions for your custom scalars.
An editable
SchemaConfiguration.swift
file.Metadata for the Apollo GraphQL executor.
Schema types are contained in their own namespace to prevent naming conflicts. You can generate this namespace as a stand-alone module, imported by your project, or as a caseless namespace enum that you can embed in your application target.
Multi-module support
Apollo iOS 1.0 can support complex applications composed of multiple modules or monolithic application targets.
The generated schema module supports multi-module projects by containing all of a schema's shared types and metadata. This enables you to move generated operation models anywhere within your project structure (as long as they are linked to the schema module).
Apollo iOS's code generation engine provides flexible configuration options that make code generation work seamlessly for any project structure.
Step-by-step instructions
Before migrating to Apollo iOS 1.0, you should consider your project structure and decide how you want to include your generated schema module and operation models.
To learn about how you can best integrate Apollo iOS to suit your project's needs, see our Project configuration documentation.
To migrate to Apollo iOS 1.0, you'll do the following:
Much of this migration process involves the new code generation mechanism.
As you go through this process, we'll explain how to remove deprecated pieces of the legacy 0.x version along the way. Each of the following steps also includes explanations for any breaking API changes.
Step 1: Upgrade to Apollo iOS 1.0
Begin by updating your Apollo iOS dependency to the latest version. You can include Apollo iOS as a package using Swift Package Manager (SPM) or Cocoapods.
To receive bug fixes and new features, we recommend including 1.0
up to the next major release.
To see the modules provided by the Apollo iOS SDK (and determine which modules you need), see SDK components.
1.package(
2 url: "https://github.com/apollographql/apollo-ios.git",
3 .upToNextMajor(from: "1.0.0")
4),
1pod 'Apollo' ~> '1.0'
Note: You can build Apollo iOS 1.0 as a dynamic
.xcframework
or a static library. You can also build and include Apollo iOS 1.0 as a pre-compiled binary with a build tool such as Carthage or Buck (though we don't currently document how to do this).
Step 2: Set up code generation
Apollo iOS 1.0 includes a new code generation engine, written in pure Swift code, replacing the legacy apollo-tooling
library. To use 1.0, you must install the new code generation engine and remove the old one.
We recommend running the new code generation engine using the Apollo Codegen CLI. You can also run code generation in a Swift script for more advanced usage.
Codegen CLI setup
For CLI setup instructions, select the method you are using to include Apollo
.
SPM with Package.swift
Install the Codegen CLI
The Apollo iOS SPM package includes the Codegen CLI as an executable target. This ensures you always have a valid CLI version for your Apollo iOS version.To simplify accessing the Codegen CLI, you can run the includedapollo-cli-install
SPM plugin. This plugin builds the CLI and creates a symbolic link to the executable in your project root.If using a Package.swift
file, you can install the CLI by running:1swift package --allow-writing-to-package-directory apollo-cli-install
apollo-ios-cli
) in your project root folder. You can now run the CLI from the command line using ./apollo-ios-cli
.Note: Because theapollo-ios-cli
in your project root is only a symbolic link, it only works if the compiled CLI executable exists. This is generally located in your Xcode Derived Data or the.build
folder. If these are cleared, you can rerun the Install CLI plugin to rebuild the CLI executable.
Initialize the code generation configuration
The Codegen CLI uses a JSON file to configure the code generation engine. You can use the Codegen CLI'sinit
command to create this file with default values.From your project's root directory, run the following command with your customized values:1./apollo-ios-cli init --schema-namespace ${MySchemaName} --module-type ${ModuleType}
${MySchemaName}
provides a name for the namespace of your generated schema files.${ModuleType}
configures how your generated schema types are included in your project.This is a crucial decision to make before configuring code generation. To determine the right option for your project, see Project Configuration.To get started quickly, you can use the
embeddedInTarget
option. UsingembeddedInTarget
, you must supply a target name using the--target-name
command line option.
apollo-codegen-config.json
file.Configure code generation options
Open yourapollo-codegen-config.json
file to start configuring code generation for your project.The default configuration will:- Find all GraphQL schema files ending with the file extension
.graphqls
within your project directory. - Find all GraphQL operation and fragment definition files ending with the file extension
.graphql
within your project directory. - Generate Swift code for the schema types in a directory with the
schema-name
provided. - Generate Swift code for the operation and fragment models in a subfolder within the schema types output location.
Run code generation
From your project's root directory, run:1./apollo-ios-cli generate
.graphql.swift
.Add the generated schema and operation files to your target
By default, a directory containing your generated schema files is within a directory with the schema name you provided (i.e.,MySchemaName
). Your generated operation and fragment files are in a subfolder within the same directory.If you created your target in an Xcode project or workspace, you'll need to manually add the generated files to your target.Note: Because adding generated files to your Xcode targets must be done manually each time you generate new files, we highly recommend defining your project targets with SPM. Alternatively, you can generate your operations into the package that includes your schema files. For more information see the documentation for Code Generation Configuration.
SPM with Xcode Project
Install the Codegen CLI
The Apollo iOS SPM package includes the Codegen CLI as an executable target. This ensures you always have a valid CLI version for your Apollo iOS version.To simplify accessing the Codegen CLI, you can run the includedInstallCLI
SPM plugin.This plugin builds the CLI and creates a symbolic link to the executable in your project root.If you use Swift packages through Xcode, you can right-click on your project in the Xcode file explorer, revealing an Install CLI plugin command. Selecting this command presents a dialog allowing you to grant the plugin "write" access to your project directory.After the plugin installs, it creates a symbolic link to the Codegen CLI (named apollo-ios-cli
) in your project root folder. You can now run the CLI from the command line with ./apollo-ios-cli
.Note: Because theapollo-ios-cli
in your project root is only a symbolic link, it only works if the compiled CLI executable exists. This is generally located in your Xcode Derived Data or the.build
folder. If these are cleared, you can rerun the Install CLI plugin to rebuild the CLI executable.
Initialize the code generation configuration
The Codegen CLI uses a JSON file to configure the code generation engine. You can use the Codegen CLI'sinit
command to create this file with default values.From your project's root directory, run the following command with your customized values:1./apollo-ios-cli init --schema-namespace ${MySchemaName} --module-type ${ModuleType}
${MySchemaName}
provides a name for the namespace of your generated schema files.${ModuleType}
configures how your generated schema types are included in your project.This is a crucial decision to make before configuring code generation. To determine the right option for your project, see Project Configuration.To get started quickly, you can use the
embeddedInTarget
option. UsingembeddedInTarget
, you must supply a target name using the--target-name
command line option.
apollo-codegen-config.json
file.Configure code generation options
Open yourapollo-codegen-config.json
file to start configuring code generation for your project.The default configuration will:- Find all GraphQL schema files ending with the file extension
.graphqls
within your project directory. - Find all GraphQL operation and fragment definition files ending with the file extension
.graphql
within your project directory. - Generate Swift code for the schema types in a directory with the
schema-name
provided. - Generate Swift code for the operation and fragment models in a subfolder within the schema types output location.
Run code generation
From your project's root directory, run:1./apollo-ios-cli generate
.graphql.swift
.Add the generated schema and operation files to your target
By default, a directory containing your generated schema files is within a directory with the schema name you provided (i.e.,MySchemaName
). Your generated operation and fragment files are in a subfolder within the same directory.If you created your target in an Xcode project or workspace, you'll need to manually add the generated files to your target.Note: Because adding generated files to your Xcode targets must be done manually each time you generate new files, we highly recommend defining your project targets with SPM. Alternatively, you can generate your operations into the package that includes your schema files. For more information see the documentation for Code Generation Configuration.
Cocoapods
Install the Codegen CLI
If you use Cocoapods, Apollo iOS compiles the Codegen CLI into an executable shell application duringpod install
(located in Pods/Apollo/apollo-ios-cli
).After installing the Apollo iOS pod, you can run the Codegen CLI from the directory of your Podfile
:1./Pods/Apollo/apollo-ios-cli ${Command Name} -${Command Arguments}
Note: If you are using :path
in your Podfile to link to a local copy of Apollo iOS, the CLI will not be automatically available. You will need to manually build the Codegen CLI. See the CLI installation guide for directions on how to do that.
Initialize the code generation configuration
The Codegen CLI uses a JSON file to configure the code generation engine. You can use the Codegen CLI'sinit
command to create this file with default values.From your project's root directory, run the following command with your customized values:1./Pods/Apollo/apollo-ios-cli init --schema-namespace ${MySchemaName} --module-type ${ModuleType}
${MySchemaName}
provides a name for the namespace of your generated schema files.${ModuleType}
configures how your generated schema types are included in your project.This is a crucial decision to make before configuring code generation. To determine the right option for your project, see Project Configuration.To get started quickly, you can use the
embeddedInTarget
option. UsingembeddedInTarget
, you must supply a target name using the--target-name
command line option.
apollo-codegen-config.json
file.Configure code generation options
Open yourapollo-codegen-config.json
file to start configuring code generation for your project.The default configuration will:- Find all GraphQL schema files ending with the file extension
.graphqls
within your project directory. - Find all GraphQL operation and fragment definition files ending with the file extension
.graphql
within your project directory. - Generate Swift code for the schema types in a directory with the
schema-name
provided. - Generate Swift code for the operation and fragment models in a subfolder within the schema types output location.
Run code generation
From your project's root directory, run:1./Pods/Apollo/apollo-ios-cli generate
.graphql.swift
.Add the generated schema and operation files to your target
By default, a directory containing your generated schema files is within a directory with the schema name you provided (i.e.,MySchemaName
). Your generated operation and fragment files are in a subfolder within the same directory.If you created your target in an Xcode project or workspace, you'll need to manually add the generated files to your target.Note: Because adding generated files to your Xcode targets must be done manually each time you generate new files, we highly recommend defining your project targets with SPM. Alternatively, you can generate your operations into the package that includes your schema files. For more information see the documentation for Code Generation Configuration.
Swift scripts setup
If you are running code generation via a Swift script, update your script to use the version of ApolloCodgenLib
that matches your Apollo
version.
Then, update the ApolloCodegenConfiguration
in your script with the new configuration values. For a list of configuration options, see Codegen configuration.
Step 3: Replace the code generation build phase
We no longer recommend running Apollo's code generation as an Xcode build phase.
Your generated files change whenever you modify your .graphql
operation definitions (which happens infrequently). Running code generation on every build increases build times and slows development.
Instead, we recommend running code generation manually (using the CLI) whenever you modify your .graphql
files.
If you want to continue running code generation on each build, you can update your build script to run the CLI generate
command.
Step 4: Refactor your code
While designing Apollo iOS 1.0, we tried to limit the number of code changes required to migrate from legacy versions.
Below are explanations for each breaking change that Apollo iOS 1.0 brings and tips on addressing those changes during your migration.
Breaking changes
Custom scalars
In the 0.x version of Apollo iOS, your schema's custom scalars are exposed as a String
type field by default. If you used the --passthroughCustomScalars
option, your generated models included the name of the custom scalar. You were responsible for defining the types passed through to your custom scalars.
In Apollo iOS 1.0, operation models use custom scalar definitions, and by default, Apollo iOS generates typealias
definitions for all referenced custom scalars. These definitions are within your schema module. The default implementation of all custom scalars is a typealias
to String
.
Custom scalar files are generated once. This means you can edit them, and subsequent code generation executions won't overwrite your changes.
To migrate a custom scalar type to Apollo iOS 1.0, do the following:
Include the type in your schema module.
Ensure the type conforms to the
CustomScalarType
protocol.Point the
typealias
definition to the new type.Or, if the type has the exact name of your custom scalar, remove the
typealias
definition.
For more details on defining custom scalars, see Custom Scalars.
Example
We define a scalar Coordinate
, which we reference in our GraphQL operations. Apollo iOS generates the Coordinate
custom scalar:
1public extension MySchema {
2 typealias Coordinate = String
3}
A custom scalar with the name Coordinate
could replace the typealias
, like so:
1public extension MySchema {
2 struct Coordinate: CustomScalarType {
3 let x: Int
4 let y: Int
5
6 public init (_jsonValue value: JSONValue) throws {
7 guard let value = value as? String,
8 let coordinates = value.components(separatedBy: ",").compactMap({ Int($0) }),
9 coordinates.count == 2 else {
10 throw JSONDecodingError.couldNotConvert(value: value, to: Coordinate.self)
11 }
12
13 self.x = coordinates[0]
14 self.y = coordinates[1]
15 }
16
17 public var _jsonValue: JSONValue {
18 "\(x),\(y)"
19 }
20 }
21}
Cache key configuration
In the 0.x version of Apollo iOS, you could configure the computation of cache keys for the normalized cache by providing a cacheKeyForObject
block to ApolloClient
.
In Apollo iOS 1.0, we replace this with a type-safe API in the SchemaConfiguration.swift
file, which Apollo iOS generates alongside the generated schema types.
To migrate your cache key configuration code, refactor your cacheKeyForObject
implementation into the SchemaConfiguration.swift
file's cacheKeyInfo(for type:object:)
function. This function needs to return a CacheKeyInfo
struct (instead of a cache key String
).
In 0.x, we recommended that you prefix your cache keys with the __typename
of the object to prevent key conflicts.
Apollo iOS 1.0 does this automatically. If you want to group cache keys for objects of different types (e.g., by a common interface type), you can set the uniqueKeyGroup
property of the CacheKeyInfo
you return.
For more details on the new cache key configuration APIs, see Custom cache keys.
Example
Given a cacheKeyForObject
block:
1client.cacheKeyForObject = {
2 guard let typename = $0["__typename"] as? String,
3 let id = $0["id"] as? String else {
4 return nil
5 }
6
7 return "\(typename):\(id)"
8}
You can migrate this to the new cacheKeyInfo(for type:object:)
function like so:
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
3 guard let id = object["id"] as? String else {
4 return nil
5 }
6
7 return CacheKeyInfo(id: id)
8 }
9}
Or you can use the JSON value convenience initializer, like so:
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
3 return try? CacheKeyInfo(jsonValue: object["id"])
4 }
5}
Local Cache Mutations
In the 0.x version of Apollo iOS, you could directly change data in the local cache using any of your generated operation or fragment models.
The APIs for direct cache access have mostly stayed the same but generated model objects are now immutable by default. You can still read cache data directly using your generated models, but to mutate cache data, you now need to define separate local cache mutation operations or fragments.
You can define a local cache mutation model by applying the @apollo_client_ios_localCacheMutation
directive to any GraphQL operation or fragment definition.
For a detailed explanation of the new local cache mutation APIs, see Direct cache access.
Separating cache mutations from network operations
By flagging a query as a
LocalCacheMutation
, the generated model for that cache mutation no longer conforms toGraphQLQuery
. This means you can no longer use that cache mutation as a query operation.Fundamentally, this is because cache mutation models are mutable, whereas network response data is immutable. Cache mutations are designed to access and mutate only the data necessary.
If our cache mutation models were mutable, mutating them outside of a
ReadWriteTransaction
wouldn't persist any changes to the cache. Additionally, mutable data models require nearly double the generated code. By maintaining immutable models, we avoid this confusion and reduce our generated code.Avoid creating mutable versions of entire query operations. Instead, define mutable fragments or queries to mutate only the fields necessary.
Example
Given an operation and write transaction from Apollo iOS 0.x versions:
1query UserDetails {
2 loggedInUser {
3 id
4 name
5 posts {
6 id
7 body
8 }
9 }
10}
1store.withinReadWriteTransaction({ transaction in
2 let cacheMutation = UserDetailsQuery()
3
4 let newPost = UserDetailsQuery.Data.LoggedInUser.Post(id: "789, body: "This is a new post!")
5
6 try transaction.update(cacheMutation) { (data: inout UserDetailsQuery.Data) in
7 data.loggedInUser.posts.append(newPost)
8 }
9})
In Apollo iOS 1.0, you can rewrite this using a new LocalCacheMutation
:
1query AddUserPostLocalCacheMutation @apollo_client_ios_localCacheMutation {
2 loggedInUser {
3 posts {
4 id
5 body
6 }
7 }
8}
1store.withinReadWriteTransaction({ transaction in
2 let cacheMutation = AddUserPostLocalCacheMutation()
3
4 let newPost = AddUserPostLocalCacheMutation.Data.LoggedInUser.Post(data: DataDict(
5 ["__typename": "Post", "id": "789", "body": "This is a new post!"],
6 variables: nil
7 ))
8
9 try transaction.update(cacheMutation) { (data: inout AddUserPostLocalCacheMutation.Data) in
10 data.loggedInUser.posts.append(newPost)
11 }
12})
Nullable Input Values
According to the GraphQL spec, explicitly providing null
as the value for an input field is semantically different from not providing a value (nil
).
To distinguish between null
and nil
, the 0.x version of Apollo iOS generated optional input values as double optional value types (??
, or Optional<Optional<Value>>
). This was confusing for many users and didn't clearly express the intention of the API.
In Apollo iOS 1.0, we replaced the double optional values with a new GraphQLNullable
wrapper enum type.
This new type requires you to indicate your input fields' value or nullability behavior explicitly. This applies to nullable input arguments on your operation definitions and nullable properties on input objects.
While this API is slightly more verbose, it provides clarity and reduced bugs caused by unexpected behavior.
For more examples and best practices using
GraphQLNullable
, see Working with nullable arguments.
Example
If we are passing a value to a nullable input parameter, we'll need to wrap that value in a GraphQLNullable
:
1MyQuery(input: "Value")
1MyQuery(input: .some("Value"))
To provide a null
or nil
value, use .null
or .none
, respectively.
1/// A `nil` double optional value translates to omission of the value.
2MyQuery(input: nil)
3
4/// An optional containing a `nil` value translates to an `null` value.
5MyQuery(input: .some(nil))
1/// A `GraphQLNullable.none` value translates to omission of the value.
2MyQuery(input: .none)
3
4/// A `GraphQLNullable.null` value translates to an `null` value.
5MyQuery(input: .null)
When passing an optional value to a nullable input value, you need to provide a fallback value if your value is nil
:
1var optionalInput: String? = nil
2
3MyQuery(input: optionalInput)
1var optionalInput: String? = nil
2
3MyQuery(input: optionalInput ?? .null)
Mocking operation models for testing
In the 0.x version of Apollo iOS, you could create mocks of your generated operation models by using each model's generated initializers or by initializing them directly with JSON data. Both methods were error-prone, cumbersome, and fragile.
Apollo iOS 1.0 provides a new way to generate test mocks based on your schema types. Begin by adding output.testMocks
to your code generation configuration, then link your generated test mocks to your unit test target.
Instead of creating a model using a type's generated initializer, you create a test mock of the schema type for the underlying object. Using the test mock, you can set values for relevant fields and initialize your operation model.
Apollo iOS 1.0's new test mocks are more comprehensible and type-safe. They also remove the need for generated initializers for different model types.
Note, you can continue initializing your operation models with JSON data, but the initializer has changed slightly. For more information, See JSON initializer.
For more details, see Test Mocks.
Examples
Given a Hero
interface type that can be either a Human
or Droid
type, and the following operation definition:
1query HeroDetails {
2 hero {
3 id
4 ... on Human {
5 name
6 }
7 ... on Droid {
8 modelNumber
9 }
10 }
11}
The 0.x version of Apollo iOS generates initializers for each type on the HeroDetails.Data.Hero
model:
1struct Hero {
2 static func makeHuman(id: String, name: String) {
3 // ...
4 }
5
6 static func makeDroid(id: String, modelNumber: String) {
7 // ...
8 }
9}
These initializers are not generated in Apollo iOS 1.0. Instead, you can initialize either a Mock<Human>
, or a Mock<Droid>
:
1let mockHuman = Mock<Human>()
2mockHuman.id = "10"
3mockHuman.name = "Han Solo"
4
5let mockDroid = Mock<Droid>()
6mockDroid.id = "12"
7mockDroid.modelNumber = "R2-D2"
Then, create mocks of the HeroDetails.Data.Hero
model using your test mocks:
1let humanHero = HeroDetails.Data.Hero(from: mockHuman)
2let droidHero = HeroDetails.Data.Hero(from: mockDroid)
Test mocks from JSON Data
If you want to continue initializing your models using JSON data directly, you can update your initializers to create your model using the init(data: DataDict)
initializer. You must also ensure that your JSON data is a [String: AnyHashable]
dictionary.
1let json: [String: Any] = [
2 "__typename: "Human",
3 // ...
4]
5
6let hero = HeroDetails.Data.Hero(
7 unsafeResultMap: json
8)
1let json: [String: AnyHashable] = [
2 "__typename: "Human",
3 // ...
4]
5
6let hero = HeroDetails.Data.Hero(
7 data: DataDict(json)
8)