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:

  1. Begin by reading about the key changes.

  2. Next, follow our step-by-step instructions to upgrade your existing app.

  3. 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:

  1. Update your Apollo iOS dependency

  2. Setup code generation

  3. Replace the code generation build phase

  4. Refactor your code to use new APIs

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.

Swift
Package.swift
1.package(
2  url: "https://github.com/apollographql/apollo-ios.git",
3  .upToNextMajor(from: "1.0.0")
4),
Ruby
Podfile
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 included apollo-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:
Bash
1swift package --allow-writing-to-package-directory apollo-cli-install
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 using ./apollo-ios-cli.
Note: Because the apollo-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's init command to create this file with default values.From your project's root directory, run the following command with your customized values:
Bash
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. Using embeddedInTarget, you must supply a target name using the --target-name command line option.
Running this command creates an apollo-codegen-config.json file.

Configure code generation options

Open your apollo-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:
Bash
1./apollo-ios-cli generate
The code generation engine creates your files with the extension .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 included InstallCLI 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.Where to find the SPM plugin commands in XcodeAfter 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 the apollo-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's init command to create this file with default values.From your project's root directory, run the following command with your customized values:
Bash
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. Using embeddedInTarget, you must supply a target name using the --target-name command line option.
Running this command creates an apollo-codegen-config.json file.

Configure code generation options

Open your apollo-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:
Bash
1./apollo-ios-cli generate
The code generation engine creates your files with the extension .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 during pod 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:
Bash
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's init command to create this file with default values.From your project's root directory, run the following command with your customized values:
Bash
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. Using embeddedInTarget, you must supply a target name using the --target-name command line option.
Running this command creates an apollo-codegen-config.json file.

Configure code generation options

Open your apollo-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:
Bash
1./Pods/Apollo/apollo-ios-cli generate
The code generation engine creates your files with the extension .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:

Swift
MySchema/CustomScalars/UUID.swift
1public extension MySchema {
2  typealias Coordinate = String
3}

A custom scalar with the name Coordinate could replace the typealias, like so:

Swift
MySchema/CustomScalars/UUID.swift
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:

Swift
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:

Swift
SchemaConfiguration.swift
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:

Swift
SchemaConfiguration.swift
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 to GraphQLQuery. 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:

GraphQL
1query UserDetails {
2  loggedInUser {
3    id
4    name
5    posts {
6      id
7      body
8    }
9  }
10}
Swift
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:

GraphQL
1query AddUserPostLocalCacheMutation @apollo_client_ios_localCacheMutation {
2  loggedInUser {
3    posts {
4      id
5      body
6    }
7  }
8}
Swift
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:

Swift
Apollo iOS 0.x
1MyQuery(input: "Value")
Swift
Apollo iOS 1.0
1MyQuery(input: .some("Value"))

To provide a null or nil value, use .null or .none, respectively.

Swift
Apollo iOS 0.x
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))
Swift
Apollo iOS 1.0
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:

Swift
Apollo iOS 0.x
1var optionalInput: String?  = nil
2
3MyQuery(input: optionalInput)
Swift
Apollo iOS 1.0
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:

GraphQL
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:

Swift
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>:

Swift
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:

Swift
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.

Swift
0.x
1let json: [String: Any] = [