Docs
Launch GraphOS Studio

Apollo iOS 1.0 migration guide

From 0.x to 1.0


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

Package.swift
.package(
url: "https://github.com/apollographql/apollo-ios.git",
.upToNextMajor(from: "1.0.0")
),
Podfile
pod '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 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.

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 by default. If you used the --passthroughCustomScalars option, your generated models included the name of the custom . 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:

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:

MySchema/CustomScalars/UUID.swift
public extension MySchema {
typealias Coordinate = String
}

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

MySchema/CustomScalars/UUID.swift
public extension MySchema {
struct Coordinate: CustomScalarType {
let x: Int
let y: Int
public 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)"
}
}
}

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:

client.cacheKeyForObject = {
guard let typename = $0["__typename"] as? String,
let id = $0["id"] as? String else {
return nil
}
return "\(typename):\(id)"
}

You can migrate this to the new cacheKeyInfo(for type:object:) function like so:

SchemaConfiguration.swift
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)
}
}

Or you can use the JSON value convenience initializer, like so:

SchemaConfiguration.swift
public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
return try? CacheKeyInfo(jsonValue: object["id"])
}
}

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 .

You can define a local cache model by applying the @apollo_client_ios_localCacheMutation 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 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 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 necessary.

Example

Given an operation and write transaction from Apollo iOS 0.x versions:

query UserDetails {
loggedInUser {
id
name
posts {
id
body
}
}
}
store.withinReadWriteTransaction({ transaction in
let cacheMutation = UserDetailsQuery()
let newPost = UserDetailsQuery.Data.LoggedInUser.Post(id: "789, body: "This is a new post!")
try transaction.update(cacheMutation) { (data: inout UserDetailsQuery.Data) in
data.loggedInUser.posts.append(newPost)
}
})

In Apollo iOS 1.0, you can rewrite this using a new LocalCacheMutation:

query AddUserPostLocalCacheMutation @apollo_client_ios_localCacheMutation {
loggedInUser {
posts {
id
body
}
}
}
store.withinReadWriteTransaction({ transaction in
let 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) in
data.loggedInUser.posts.append(newPost)
}
})

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

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

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

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

When passing an optional value to a nullable input value, you need to provide a fallback value if your value is nil:

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

query HeroDetails {
hero {
id
... on Human {
name
}
... on Droid {
modelNumber
}
}
}

The 0.x version of Apollo iOS generates initializers for each type on the HeroDetails.Data.Hero model:

struct Hero {
static func makeHuman(id: String, name: String) {
// ...
}
static func makeDroid(id: String, modelNumber: String) {
// ...
}
}

These initializers are not generated in Apollo iOS 1.0. Instead, you can initialize either a Mock<Human>, or a Mock<Droid>:

let mockHuman = Mock<Human>()
mockHuman.id = "10"
mockHuman.name = "Han Solo"
let mockDroid = Mock<Droid>()
mockDroid.id = "12"
mockDroid.modelNumber = "R2-D2"

Then, create mocks of the HeroDetails.Data.Hero model using your test mocks:

let humanHero = HeroDetails.Data.Hero(from: mockHuman)
let 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.

0.x
let json: [String: Any] = [
"__typename: "Human",
// ...
]
let hero = HeroDetails.Data.Hero(
unsafeResultMap: json
)
1.0
let json: [String: AnyHashable] = [
"__typename: "Human",
// ...
]
let hero = HeroDetails.Data.Hero(
data: DataDict(json)
)
Previous
SDK components
Next
v1.2
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company