Apollo iOS 2.0 Migration Guide
Introduction
Apollo iOS 2.0 represents a major evolution of the library, redesigned from the ground up to take full advantage of Swift's modern concurrency model. This release introduces significant breaking changes that require careful migration planning, but provides substantial improvements in type safety, performance, and developer experience.
What's new in 2.0
Apollo iOS 2.0 brings several foundational changes:
Swift Concurrency First: Complete reimplementation using
async/awaitand structured concurrencyStreamlined APIs: Simplified client APIs with clearer separation of concerns and more precise type safety
Modern Swift Features: Full adoption of Swift 6 with strict concurrency enabled
Breaking changes overview
This migration involves significant API changes across several areas:
ApolloClient API: Complete redesign using
async/awaitinstead of callback-based APIsCache Policies: Split into discrete types with specific return signatures
Request Interceptors: New framework with separate interceptor types for different request phases
Sendable Types: Most types now conform to
Sendablewith associated constraints
Limitations
Minimum Deployment Targets
Apollo iOS 2.0 drops support for older deployment targets. The minimum deployment targets are now:
iOS 15.0+
macOS 12.0+
tvOS 15.0+
watchOS 8.0+
visionOS 1.0+
CocoaPods Support Removed
Cocoapods support is no longer available in Apollo iOS 2.0. The library is available via Swift Package Manager or as .xcframeworks built using the Apollo iOS XCFramework Repo.
Code generation of Cocoapods modules is no longer supported.
Web socket support (Coming Soon)
The initial release of 2.0 will not support web sockets. Subscriptions over HTTP are supported by RequestChainNetworkTransport.
A new implementation of the ApolloWebSocket target will be published in a future version as soon as possible. If your application requires web sockets, you should not upgrade to 2.0 until the new web socket implementation is released.
Migration strategy
To migrate to Apollo iOS 2.0, first update your version dependency and re-run code generation using 2.0 code-gen engine.
After you've updated, migration follows a two-phase approach designed to minimize disruption while ensuring all functionality is properly updated.
Phase 1: Breaking changes
Some APIs have fundamentally changed to support the new structured concurrency model and cannot maintain backward compatibility. You must update these implementations before proceeding with the rest of your migration.
These changes include:
Various changes to request/response models
RequestChainconfiguration via customInterceptorsCustom
NetworkTransportimplementationsCustom
NormalizedCacheimplementations
Phase 2: Incrementally update ApolloClient API usage
The most commonly used APIs for fetching data via ApolloClient have been deprecated instead of removed completely. These APIs have been updated to conform to Sendable, so you will need to resolve thread-safety issues. But once breaking changes have been resolved, this allows you to:
Compile and run your application with the deprecated APIs while planning your migration
Update incrementally - migrate one query, mutation, or subscription at a time
Verify behavior at each step to ensure correctness before proceeding
Deploy intermediate states if needed during a gradual rollout
While we recommend that you update these deprecated APIs to the new versions ASAP, you can do so incrementally while still compiling your application and verifying behavior. You should update these APIs as soon as possible to take advantage of the use of async/await and increased type safety of the return types for these APIs.
The remainder of this guide provides detailed instructions for migrating each aspect of your Apollo iOS integration.
Codegen Configuration
Apollo iOS 2.0 removes a number of obsolete options from the codegen configuration. Your configuration will need to be updated to remove these options as well.
If you are using a JSON codegen configuration file, you may get an error when attempting to run codegen. If using the ApolloCodegenLib directly in Swift, your configuration may not compile.
The following changes have been made to the codegen configuration:
OutputOptions.cocoapodsCompatibleImportStatements: CocoaPods support has been completely removed.OutputOptions.markOperationDefinitionsAsFinal: Generated GraphQL operation and local cache mutation class types are alwaysfinalnow to supportSendableconformance. SubclassingGraphQLOperationsis no longer permitted.OutputOptions.apqs: The legacyAPQConfighas been replaced byoperationDocumentFormat. Update your configuration to useOperationDocumentFormat.operationIdinstead ofAPQConfig.persistedOperationsOnly.SchemaTypesFileOutput.ModuleType.swiftPackageManager: This deprecated case has been removed. Use.swiftPackage(apolloSDKDependency: .default)instead.
URLSessionClient -> ApolloURLSession
In Apollo iOS 1.0 a URLSessionClient that managed an internal URLSession was used to handle network operations. In 2.0, this is replaced by the ApolloURLSession protocol. URLSession conforms to this procotol by default. This allows you to provide your own URLSession to the RequestChainNetworkTransport. You can configure this URLSession as needed and provide your own delegates to it.
1let urlSession = URLSession.shared // Conforms to ApolloURLSession
2urlSession.delegate = self
3let networkTransport = RequestChainNetworkTransport(
4 interceptorProvider: DefaultInterceptorProvider.shared,
5 endpointURL: url,
6 urlSession: urlSession
7)RequestContext -> Task Local Values
In Apollo iOS 1.x, you could provide an arbitrary RequestContext when initiating a GraphQL operation. This could be accessed by custom interceptors or NetworkTransport implementations.
2.0 deprecates RequestContext in favor of using Swift 6's @TaskLocal values. For more information on using @TaskLocal values with your network configuration, see our Request Chain @TaskLocal values documentation.
RequestChain configuration
The RequestChain and interceptor framework has been completely reimagined. The new version supports async/await and provides the ability to interact with the request at each step within the chain more safely with more explicit APIs.
If you are providing your own custom InterceptorProvider with your own interceptors, you will need to modify your code to utilize these new APIs.
This migration guide provides an overview of the changes. For detailed documentation on the new request chain, see the Request Chain Configuration documentation.
Before migrating your InterceptorProvider to 2.0, we recommend you familiarize yourself with these key changes to the Request Chain.
Discreet Interceptors Types
The singular ApolloInterceptor protocol that was used to handle any step of the request chain has been broken up into discrete interceptor types for different portions of request execution. This provides a cleaner, more type-safe API for interacting with the request and response at different stages of the RequestChain.
The four interceptor types are:
GraphQLInterceptorCan inspect and mutate the
GraphQLRequestandGraphQLResponse
HTTPInterceptorCan inspect and mutate the
URLRequestAfter network response can inspect the
HTTPURLResponse(readonly) and mutate the actual raw responseDataprior to parsing
CacheInterceptorHandles read/write of cache data
Read currently runs before
GraphQLInterceptors(not sure if that is the desired behavior, we should discuss)Write runs after parsing
ResponseParsingInterceptorHandles the parsing of the response Data into the
GraphQLResponse
NetworkFetchInterceptor is no longer used, as the network fetch is managed by the ApolloURLSession. See the section on ApolloURLSession for more information.
Request Chain Flow
In 1.0, interceptors were called once in sequential order. In 2.0, requests are now sent down the request chain pre-flight and then back up the chain post-flight, allowing each interceptor to interact with both the request and response in a type-safe way.
Requests are now processed by the RequestChain using the following flow:
GraphQLInterceptorsreceive and may mutateRequestCache read executed via
CacheInterceptorif necessary (based on cache policy)GraphQLRequest.toURLRequest()called to obtainURLRequestHTTPInterceptorsreceive and may mutateURLRequestApolloURLSessionhandles networking withURLRequestHTTPInterceptorsreceive stream ofHTTPResponseobjects for each chunk & may mutate raw chunkDatastreamResponseParsingInterceptorreceivesHTTPResponseand parses data chunks into stream ofGraphQLResponseGraphQLInterceptorsreceive and may mutateGraphQLResponsewith parsedGraphQLResultand (possibly) cache records.Cache write executed via
CacheInterceptorif necessary (based on cache policy)GraphQLResponseemitted out toNetworkTransport
Error Handling
The ApolloErrorInterceptor has been removed from 2.0. Instead, any GraphQLInterceptor can intercept errors by calling the mapErrors function of the InterceptorResultStream. Errors can even be intercepted by multiple interceptors that perform different error handling logic.
For more information on error handling see the Request Chain error handling documentation.
Retrying Requests
In 1.0, you could retry a RequestChain's request by calling chain.retry(request:completion). In Apollo iOS 2.0, interceptors no longer have access to the RequestChain itself. Instead, interceptors can trigger a retry by throwing a RequestChain.Retry error.
For more information on retrying see the Request Chain request retries documentation.
Converting 1.x interceptor providers
Converting your existing custom InterceptorProvider to 2.0 requires two main steps:
Refactor existing interceptors to use one of the four new discrete interceptor types
Update your
InterceptorProviderto conform to the new protocol structure
Step 1: Refactor existing interceptors
In 1.x, all custom logic was implemented using the ApolloInterceptor protocol. In 2.0, you need to choose the appropriate interceptor type based on what your interceptor does. See the linked documentation for each interceptor type for more information on how to implement them.
GraphQLInterceptor: Provides access to theGraphQLRequestand the parsedGraphQLResponse. Most interceptors from 1.0 should use this type.HTTPInterceptor: Provides access to theHTTPURLResponseand the raw data stream of the network response.CacheInterceptor: To implement custom caching logic use aCacheInterceptor.ResponseParsingInterceptor: Parses raw response data into aGraphQLResponseand set of records for writing to the cache.
Step 2: Update your InterceptorProvider
Once your interceptors are refactored for the new APIs, update your InterceptorProvider.
The InterceptorProvider methods provide default implementations that use the DefaultInterceptorProvider values. This means you only need to override the methods that you would like to provide custom interceptors for.
It is strongly recommended that you include the default GraphQL and HTTP interceptors provided by DefaultInterceptorProvider.shared.
DefaultInterceptorProvider has been changed to a final class to support Sendable. If you were previously subclassing DefaultInterceptorProvider you will need to access it inside of your InterceptorProvider instead.
1struct CustomInterceptorProvider: InterceptorProvider {
2
3 // Provide GraphQL interceptors (pre/post-flight GraphQL processing)
4 func graphQLInterceptors<Operation: GraphQLOperation>(for operation: Operation) -> [any GraphQLInterceptor] {
5 return DefaultInterceptorProvider.shared.graphQLInterceptors(for: operation) + [
6 AuthInterceptor(token: getCurrentToken()),
7 LoggingInterceptor()
8 ]
9 }
10
11 // Provide cache interceptor (cache read/write operations)
12 func cacheInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> any CacheInterceptor {
13 return CustomCacheInterceptor()
14 }
15
16 // Provide HTTP interceptors (URLRequest/HTTPResponse processing)
17 func httpInterceptors<Operation: GraphQLOperation>(for operation: Operation) -> [any HTTPInterceptor] {
18 return DefaultInterceptorProvider.shared.httpInterceptors(for: operation) + [
19 CustomHeaderInterceptor()
20 ]
21 }
22}In this example, we are not providing an implementation of responseParser(for operation:), so the default JSONResponseParsingInterceptor will be used.
Minor Breaking Changes
Apollo iOS 2.0 introduces a number of minor breaking changes that may affect your migration depending on your API usage.
Sendable conformance and thread safety
Apollo iOS 2.0 adopts Swift 6's strict concurrency model and most types in the library now conform to Sendable. This ensures that Apollo iOS operations are safe to use across different threads and with Swift's structured concurrency model. We recommend migrating to the new async/await fetch APIs to take full advantage of these safety improvements.
In order to provide Sendable conformance and full thread safety, some types have been converted from class to struct types.
Most core Apollo iOS types now conform to Sendable, including:
ApolloClientApolloStoreCachePolicytypesRequestConfigurationNetworkTransportimplementationsGenerated operation and fragment models
Interceptor types
HTTPResponseGraphQLResponse
GraphQLResponse & GraphQLResult
Apollo iOS 1.0 exposed both a GraphQLResponse and GraphQLResult model. The GraphQLResponse was mostly used internally for parsing of response data, while GraphQLResult was the response model for a GraphQL request exposed to the user.
In 2.0, the legacy GraphQLResponse model has been removed and GraphQLResult has been renamed to GraphQLResponse. Additionally, the new response model is generic over the operation instead of the response data. This allows access to type information about the GraphQL operation as well as it's response data.
Code referencing the GraphQLResult directly will need to be refactored to reference the new GraphQLResponse instead.
If you were using the old GraphQLResponse for manual response parsing, use JSONResponseParser instead.
Cache misses no longer throw errors
When a cache miss occurred in Apollo iOS 1.0, the ApolloStore would throw an error (usually a JSONDecodingError.missingValue). In 2.0, cache misses result in a nil value being returned. Errors are only thrown if there is an issue with the cache such as invalid data or a disk I/O failure.
Cache read methods on ApolloStore and in ReadTransactions have been updated to return Optional results. You may need to update cache transation usage to adjust for this new behavior.
SPI usage
Many of the APIs that are not intended for use general public use are now protected by the @_spi attribute. If you are using any of the public APIs that were prefixed with an _ or marked as Internal Use Only, you will need to import the SPI in order to continue using them.
Apollo uses the following SPI categories:
Execution Exposes symbols used by the generated models but not intended for public consumption.
Internal Exposes symbols used by
ApolloandApolloAPIacross module boundaries but not intended for public consumption.Unsafe Exposes symbols that provide unsafe access to the underlying data of the GraphQL models. The public API provides strongly type access to the data that is guaranteed to be safe. If invalid data is provided to the models by using unsafe APIs you may cause crashes or other undefined behavior.
Custom Scalar Types
Custom implementations of CustomScalarType need to import the Execution and Internal SPIs. Newly generated custom scalars will have the correct import statement, but existing custom scalars are not overwritten when code generation is ran.
You may see Type 'MyCustomScalar' does not conform to protocol ... errors while upgrading. To resolve these, change the import statement in those custom scalar files to:
1@_spi(Execution) @_spi(Internal) import ApolloAPIGraphQL Int32 input values
In Apollo iOS 1.x, input values with a GraphQL Int type were mapped to Swift's Int type. Starting in Apollo iOS 2.0, these are now mapped to Swift's Int32 type to align with the GraphQL specification, which defines the Int type as a signed 32-bit integer. This change only effects input values (ie. query parameters & fields on input objects). Int fields on response models will still use the Swift Int type.
This change ensures proper type safety and prevents potential overflow issues when working with GraphQL APIs that rely on the spec-compliant 32-bit integer.
While migrating, you will need to convert your Int values to Int32 when passing them as input parameters to a GraphQL operation or input object.