Apollo iOS 1.2 migration guide
From 1.0 - 1.1 to 1.2
This guide describes the process of migrating your code from version 1.0 or 1.1 to version 1.2 of Apollo iOS.
Though 1.2 is a minor version bump, a critical problem was addressed in this version that requires a small breaking change during the upgrade. While we strive to make the upgrade path for minor versions seamless, this issue could not be reasonably resolved without requiring this migration.
Cache Key Configuration API
The API for configuring custom cache keys has had a minor change in this version. The signature of the cacheKeyInfo(for:object:)
function, defined in your generated SchemaConfiguration.swift
file, has been modified.
In previous versions, the object
parameter was of the type JSONObject
, which is a typealias
for the type Dictionary<String, AnyHashable>
. In 1.2, the object
parameter is of a newly defined type ObjectData
.
In order to preserve your implementation, the SchemaConfiguration.swift
file is generated only the first time code generation is run. This means that when upgrading to Apollo iOS 1.2, you will need to modify the function signature of the cacheKeyInfo(for:object:)
function.
For new projects, the function will be properly generated on the first code generation run.
Reasoning
Because cache key resolution is performed both on raw JSON (from a network response) and SelectionSet
model data (when writing to the cache directly), the underlying object
data will have different formats. This inconsistency caused bugs that were difficult to track down for some users. The ObjectData
wrapper struct
is used to normalize this data to a consistent format in the context of the cacheKeyInfo(for:object:)
function.
Migration Steps
For most users, the change will be as simple as changing the function signature from:
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
3 /// ...
4 }
5}
to
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3 /// ...
4 }
5}
For most basic use cases, which only use the object
's subscript
to access its values, this will suffice. However, the ObjectData
struct does not provide the same API as a Dictionary
and has some semantic differences that you may need to address. We recommend running sufficient tests to ensure your cache key resolution function still returns the expected values.
If your cache key resolution function does not compile or resolve cache keys correctly with the new function signature, please continue reading for more information.
Changes to underlying value types
The ObjectData
struct allows you to access the values of the provided object
in a way that ensure consistency of the value types. If your implementation of the cacheKeyInfo(for:object:)
function casts values to specific expected types, you may need to modify your code to ensure that casting is done properly. The values you should expect to be returned for the keys in an ObjectData
struct are as follows:
Scalar types
The raw scalar value (
String, Int, Float, Double
orBool
)
Custom scalar types
The custom scalar will be deserialized into the raw value returned from the type's
jsonValue
property.See Custom Scalars for more information
Object types
For other GraphQL objects nested inside of the passed
object
, anotherObjectData
struct will be returned.In previous versions, the return value was another
JSONObject
(ie.Dictionary<String, AnyHashable>
).
List types
For a list field, the returned value is the newly defined
ListData
struct.ListData
functions the same asObjectData
, wrapping an array of elements instead of a dictionary.
ObjectData
API limitations
The ObjectData
struct exposes a notably limited API. It only allows subscript access to the key value pairs of its underlying data. Generally, this API should suffice for accessing the values needed to resolve cache keys. If your cacheKeyInfo(for:object:)
function is using any other functionality of Dictionary
, you will need to refactor your code.
In order to limit the complexity required by cache key resolution mechanisms, additions to the ObjectData
API are being made only as needed based on feedback from our users. If your cacheKeyInfo(for:object:)
function requires other functionality to be exposed on ObjectData
, please submit a Feature Request.
Example
Let's consider an example cacheKeyInfo(for:object:)
function from the previous version which needs to have some modifications made while migrating to version 1.2
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
3 switch type {
4 case Objects.User:
5 let userInfo = object[info] as? [String: AnyHashable]
6 let emailList = userInfo["emailAddresses"] as? [String]
7 return try? CacheKeyInfo(jsonValue: emailList[0])
8
9 case Objects.Post:
10 let timestampCustomScalar = object["timestamp"] as? TimeStamp
11 let timestampRawValue = object["timestamp"] as? String
12 let timestampString = (timestampCustomScalar?.jsonValue as? String)
13 ?? timestampRawValue
14 return try? CacheKeyInfo(jsonValue: timestampString)
15
16 case Objects.Product:
17 return try? CacheKeyInfo(id: object["UPC"])
18
19 default:
20 return nil
21 }
22 }
23}
To migrate this function to 1.2, we need to make a few changes.
The
object
parameter must be changed to the typeObjectData
The value for the
User
object'sinfo
field should be cast toObjectData
instead of[String: AnyHashable]
The
User
object'sinfo.emailAddresses
should expect aListData
type instead of[String]
.Previously, the value for the
Post
object'stimestamp
custom scalar may have been theTimeStamp
custom scalar or its rawjsonValue
type, depending on the source of the data. Now we should always expect the deserializedjsonValue
– in this case, aString
.The
Product
object'sUPC
field has always been a scalarString
value. There is no change needed here.
1public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
2 static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
3 switch type {
4 case Objects.User:
5 let userInfo = object["info"] as? ObjectData
6 let emailList = userInfo["emailAddresses"] as? ListData
7 return try? CacheKeyInfo(jsonValue: emailList[0] as? String)
8
9 case Objects.Post:
10 let timestamp = object["timestamp"] as? String
11 return try? CacheKeyInfo(jsonValue: timestamp)
12
13 case Objects.Product:
14 return try? CacheKeyInfo(jsonValue: object["UPC"])
15
16 default:
17 return nil
18 }
19 }
20}
The ObjectData
and ListData
subscript accessors can also be chained together. This means the User
object's cache key resolution could be implemented more simply as:
1case Objects.User:
2 return try? CacheKeyInfo(
3 jsonValue: object["info"]?["emailAddresses"]?[0]
4 )
Swift Access Modifiers
1.2.0
introduces new codegen configuration parameters that allow you to specify the access control of the generated Swift types. When using a module type of embeddedInTarget
or operation output types of relative
or absolute
you can choose to have the generated Swift types be accessible with public
or internal
access.
You do not need to add these options to your codegen configuration but the default used when the option is not specified is different from previous Apollo iOS versions.
Before 1.2.0
all Swift types were generated with public
access, the default for the new configuration option is internal
.
This means that where you might have been using publicly available Swift types before you might now have compiler errors where those types are no longer accessible. To resolve this you will need to add the configuration option to your codegen configuration specifying the public
access modifier.
You may need to make manual changes to the schema configuration and custom scalar files because these files are not regenerated if they already exist. The alternative to manually updating them is to remove those files, run code generation, and then re-add any custom logic you may have had in the pre-existing custom scalar files.
Example
Module type
1"output": {
2 "schemaTypes": {
3 "moduleType": {
4 "embeddedInTarget": {
5 "name": "MyApplicationTarget",
6 "accessModifier": "public"
7 }
8 },
9 "path": "./generated/schema/"
10 }
11}
1let configuration = ApolloCodegenConfiguration(
2 // Other properties not shown
3 output: ApolloCodegenConfiguration.FileOutput(
4 schemaTypes: ApolloCodegenConfiguration.SchemaTypesFileOutput(
5 path: "./generated/schema/",
6 moduleType: .embeddedInTarget(name: "MyApplicationTarget", accessModifier: .public)
7 )
8 ...
9 )
10)
Operations - relative
1"output": {
2 "operations" : {
3 "relative" : {
4 "subpath": "Generated",
5 "accessModifier": "public"
6 }
7 }
8}
1let configuration = ApolloCodegenConfiguration(
2 // Other properties not shown
3 output: ApolloCodegenConfiguration.FileOutput(
4 operations: .relative(
5 subpath: "generated",
6 accessModifier: .public
7 )
8 ...
9 )
10)
Operations - absolute
1"output": {
2 "operations" : {
3 "absolute" : {
4 "path": "Generated",
5 "accessModifier": "public"
6 }
7 }
8}
1let configuration = ApolloCodegenConfiguration(
2 // Other properties not shown
3 output: ApolloCodegenConfiguration.FileOutput(
4 operations: .absolute(
5 path: "generated",
6 accessModifier: .public
7 )
8 ...
9 )
10)