This is the first preview release of Apollo iOS 2.0. This preview release contains APIs that are still in development and are subject to change prior to stable release.
This version is likely to contain bugs and some features are still limited. This preview is intended to allow interested users to test out the new APIs and provide feedback to help shape the final product.
We are looking for bug reports as well as use cases that may not be supported by the current APIs. Any general feedback on the project is welcome as well. Bug reports can be filed as GitHub issues. For feature requests and general feedback, please comment on the 2.0 RFC Megathread.
Support for web sockets is not included in this preview release and will be implemented in a future release after 2.0.0. In the interim, WebSocketNetworkTransport has been temporarily replaced with a stubbed type that throws an error. Subscriptions are still supported over HTTP via the RequestChainNetworkTransport.
Support for pagination using the ApolloPagination package is not included in this preview release and will be implemented prior to the first Beta release.
This preview is available now under the tag 2.0.0-alpha-1. To try out the alpha, modify your SPM dependency to:
.package(
url: "https://github.com/apollographql/apollo-io.git", Version(2, 0, 0, prereleaseIdentifiers: ["alpha"]),
Many of the existing APIs from Apollo iOS 1.0 have been marked as deprecated, with only the minimal necessary modifications to compile Apollo iOS 2.0. These APIs are untested with the new underlying infrastructure and may not be reliable. All deprecated APIs will be removed prior to the stable release of 2.0. These APIs still exist only to aid users in the migration to the new APIs. By deprecating these APIs instead of just removing them, we hope that it will make it easier to incrementally migrate your codebase to Apollo iOS 2.0.
Apollo iOS 2.0 reimagines many of the APIs to take full advantage of the new Swift concurrency model. This is a non-exhaustive list of the key changes:
ApolloClient & CachePolicyThe APIs of ApolloClient have changed significantly to use async/await. Rather than providing a resultHandler closure that may be called one or more times, separate APIs are defined depending on if an operation expects single/multiple responses. CachePolicy has been broken up into multiple types that will automatically force the function with the correct return signature.
// Single response
let response = try await client.fetch(query: query, cachePolicy: .cacheFirst)
let response = try await client.fetch(query: query, cachePolicy: .networkFirst)
let response = try await client.fetch(query: query, cachePolicy: .networkOnly)
// Single response with Optional return value
let response = try await client.fetch(query: query, cachePolicy: .cacheOnly)
// Multiple responses
// Returns an AsyncThrowingStream<GraphQLResponse<Query>, any Swift.Error>
let responses = try client.fetch(query: query, cachePolicy: .cacheAndNetwork)
Task {
for try await response in responses {
// Handle response
}
}
Subscriptions and operations that provide incremental data (via the @defer directive and in the future @stream), will always return an AsyncThrowingStream<GraphQLResponse<Query>, any Swift.Error> of responses unless using the .cacheOnly policy.
let responses = try client.fetch(query: deferQuery, cachePolicy: .cacheFirst) // -> AsyncThrowingStream
let responses = try client.fetch(query: deferQuery, cachePolicy: .networkFirst) // -> AsyncThrowingStream
let responses = try client.fetch(query: deferQuery, cachePolicy: .networkOnly) // -> AsyncThrowingStream
let responses = try client.fetch(query: deferQuery, cachePolicy: .cacheAndNetwork)
Task {
for try await response in responses {
// Handle response
}
}
let response = try await client.fetch(query: deferQuery, cachePolicy: .cacheOnly) // async throws -> GraphQLResponse<DeferQuery>?
The for try await response in responses loop will continue to run until the operation is complete. For subscriptions, this may be indefinite. For this reason, the returned stream should be consumed within a Task.
Sendable TypesIn order to support the new Swift concurrency model, most of the types in Apollo iOS have been made Sendable. In order to make these types Sendable, some limitations were necessary.
var properties have been converted to constant let properties. We don't believe this should prevent users from accessing any necessary functionality, but we are seeking feedback on the effect this change has on your usage.open classes have been changed to final classes or structs. This prevents subclassing types such as RequestChainNetworkTransport, InterceptorProvider, JSONRequest, and others. If you are currently subclassing these types, you will need to convert your existing subclasses to wrappers that wrap these types and passthrough calls to them instead.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.
The singular ApolloInterceptor 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. Additionally, requests are sent down the request chain pre-flight and then back up the chain post-flight, allowing each interceptors to interact with the both the request and response in a type-safe way.
ApolloInterceptor has been separated into 4 different interceptor types.
GraphQLInterceptor
GraphQLRequest and GraphQLResponseHTTPInterceptor
URLRequestHTTPURLResponse (readonly) and mutate the actual raw response Data prior to parsingCacheInterceptor
GraphQLInterceptors (not sure if that is the desired behavior, we should discuss)ResponseParsingInterceptor
GraphQLResponseNetworkFetchInterceptor is no longer used, as the network fetch is managed by the ApolloURLSession. See the section on ApolloURLSession for more information.
Requests are now processed by the RequestChain using the following flow:
GraphQLInterceptors receive and may mutate RequestCacheInterceptor if necessary (based on cache policy)GraphQLRequest.toURLRequest() called to obtain URLRequestHTTPInterceptors receive and may mutate URLRequestApolloURLSession handles networking with URLRequestHTTPInterceptors receive stream of HTTPResponse objects for each chunk & may mutate raw chunk Data streamResponseParsingInterceptor receives HTTPResponse and parses data chunks into stream of GraphQLResponseGraphQLInterceptors receive and may mutate GraphQLResponse with parsed GraphQLResult and (possibly) cache records.CacheInterceptor if necessary (based on cache policy)GraphQLResponse emitted out to NetworkTransportGraphQLResponse and HTTPResponse separatedPreviously, there was a single GraphQLResponse which included the HTTPResponse and optionally the ParsedResult (if the parsing interceptor had been called already). Now, since different interceptors will be called pre/post parsing, we have separate types for these response objects.
ApolloErrorInterceptorThe ApolloErrorInterceptor protocol has been removed. Instead, any GraphQLInterceptor can handle errors using .mapErrors(). If any following interceptors, or the ApolloURLSession throw an error, the mapErrors closures will be called. You can then re-throw it; throw a different error; or trigger a retry by throwing a RequestChain.Retry error. If you would like to use a dedicated error handling interceptor, it is recommended to place it as the first interceptor returned by your provider to ensure all errors thrown by the chain are handled.
RequestChain.RetryInterceptors are no longer provided a reference to the RequestChain, so they cannot call RequestChain.retry(request:) directly. Instead, any interceptor may throw a RequestChain.Retry error that contains the request to kick-off the retry with. This error is caught internally by the RequestChain which initiates a retry.
The network fetch is now managed by an ApolloURLSession provided to the ApolloClient. For your convenience, Foundation.URLSession already conforms to the ApolloURLSession protocol. This allows you to provide your own URLSession and have complete control over the session's configuration and delegate.
You may alternatively provide any other object that conforms to ApolloURLSession, wrapping the URLSession or providing an entirely separate networking stack.
async FunctionsMany of the public protocols in Apollo iOS have been modified to use async functions. If you have custom implementations of these types, they will need to be modified to use async/await instead of resultHandler closures.
This includes ApolloStore, NormalizedCache, NetworkTransport, and all interceptor types.
requireNonOptionalMockFields flag to ApolloCodegenConfiguration.OutputOptions. (#669): Added new flag to codegen output options to allow having non-optional fields in the test mocks if desired. Thank you to @dwroth for the contribution.DatabaseRow. (#664): Not having a public initializer on DatabasRow was hindering the ability to create custom SQLiteDatabase implementations. This solves that by adding a public initializer to DatabaseRow.Thank you to @ChrisLaganiere for the contribution.@_disfavoredOverload to the deprecated initialized in ApolloCodegenConfiguration to prevent possible warnings caused by the compiler selecting a deprecated initializer versus the new/current initializer. See PR #682. Thank you to @CraigSiemens for raising the issue.ReadTransaction (#661): Some users have use cases for accessing a custom NormalizedCache implementation directly while performing cache transactions. A new ReadOnlyNormalizedCache protocol exposes the cache as read-only in the ReadTransaction and as writable in the ReadWriteTransaction. See PR #661.all field merging causes selection set initializers to stop being generated for local cache mutations (#3554): Codegen will now force field merging behaviour and selection set initializer generation for local cache mutations. See PR #654.extensions key of each request. This Enhanced Client Awareness metric is collected in GraphOS along with the existing Client Awareness and general operation metrics.SQLite.swift and replaced it with direct interaction with the SQLite C API.subscriptions property inside of WebSocketTransport. Thank you to @tahirmt for the contribution.wrongType error if stored and read from the cache. This refactors the execution logic to correctly handle values from cache references in lists. See PR #637.mutateIfFulfilled function was added to facilitate that workflow while still preventing a fragment from being added or removed from an existing model. See PR #608.URLRequest timeout interval (#3522): Added a request context specialization protocol (RequestContextTimeoutConfigurable) that specifies options for configuring the timeout interval of a URLRequest. See PR #618.Object types that are generated so that only types that are referenced in an operation document or have a @typePolicy will be generated. See PR #601.InputObject needed a GraphQLNullable-specific subscript to prevent nil value keys being forcefully unwrapped. See PR #596. Thank you to @pixelmatrix for raising the issue.WebSocketTransport due to data races (#3512): This data race would occur if starting or stopping a subscription at the same time as a message received on the websocket. To prevent these data races the subscribers property is now an @Atomic property. See PR #599. Thank you to @tahirmt for the contribution.appendSchemaTypeFilenameSuffix) to add a suffix to schema generated filenames and prevent the build error. See PR #580.typePolicy directive (#554): The @typePolicy directive lets you specify an object's cache ID using key fields of the response object. See the documentation for full details. Thank you to @x-sheep for the contribution.Identifiable conformance on SelectionSet (#584): If the @typePolicy of a type uses a keyField of id the selection set will emit conformance to Swifts Identifiable protocol. Thank you to @x-sheep for the contribution.EntitySelectionSet while building operations. See PR #571.embeddedInTarget and other module types. See PR #581.DataDict initialization of deferredFragments for named fragments (#587): When deferred fragments are named fragments the deferred type should be the fragment generated definition name.@oneOf input object support (#537): Adding support for @OneOf Input Objects, more info can be found in the official RFC.URLRequest cache policy default changed (#550): The updated default closer matches the original behaviour before the introduction of setting URLRequest.CachePolicy. Thank you to @marksvend for raising the issue.DataDict initialization of deferredFragments property (#557): Generated selection set initializers were not correctly setting deferred fragment identifiers. This only affected selection sets that were instantiated with the generated selection set initializers, response-based results are unaffected.AnyHashable coercion for non-iOS platforms (#517): Extended the _AnyHashableCanBeCoerced check to include macOS, watchOS, and tvOS with their respective minimum versions. Thank you to @VMLe for the fix.GraphQLOperation hash uniqueness (#530): Adding uniqueness to GraphQLOperation hashing.URLRequest cache policy on GET requests (#476): Uses the Apollo cache policy to set a comparable cache policy on URLRequest. Previously there was no way to opt-out of default URLRequest caching behaviour.insertMany to batch write records for a given operation vs previously performing a write for each individual record.selectionSetInitializer option localCacheMutations (#467): This option was deprecated in 1.15.0, and the removal of the code to parse the option resulted in a validation error when the deprecated option was present in the JSON code generation config file. This is now fixed so that the option is ignored but does not cause code generation to fail.watch function matched the overload of the current version if certain parameters were omitted. This caused an incorrect deprecation warning in this situation. We've fixed this by adding @_disfavoredOverload to the deprecated function signature.ApolloCodegenConfiguration option to allow for disabling fragment field merging on generated models. For more information on this feature see the notes here.legacyResponse property not being set on HTTPResponse (#456): When the legacyResponse property of HTTPResponse was deprecated setting the value was also removed; this was incorrect as it created a hidden breaking change for interceptors that might have been using the value.ObjectData type check (#459): Fixed bool type check in ObjectData.SelectionSetTemplate scope comparison (#460): Refactored the selection set template scope comparison to account for an edge case in merged sources.ApolloStore caused by implicit use of self. Thank you to @prabhuamol for finding and fixing this.This is a preview release of a new feature that improves the code generation engine's field merging algorithm and allows for disabling of field merging altogether. The feature work for this preview version is being tracked in issue #2560.
The code generation algorithm is now able to recognize most situations where a merged selection set is a direct copy of a selection set that is being merged. In those cases, it now uses a typealias referencing the original selection set rather than generating a duplicate. This is most commonly seen for the child entities of a named fragment that is spread into another selection set. In some cases this can dramatically decrease the size and complexity of the generated models.
This also fixes a bug when using @include/@skip where generated models that should have been generated inside of a conditional inline fragment were generated outside of the conditional scope. This may cause breaking changes for a small number of users. Those breaking changes are considered a bug fix since accessing the conditional inline fragments outside of the conditional scope could cause runtime crashes (if the conditions for their inclusion were not met).
If you need to further reduce the size of generated models, you can use the new experimental field merging configuration option to disable field merging.
The field merging feature has three types of merging that you can enable or disable selectively:
Disabling of field merging is incompatible with the inclusion of selectionSetInitializers. Because the generated initializers require fully formed objects with all field merged into them in order to ensure the generated objects are valid for each of their type cases. It is likely that this limitation will not be able to be resolved in the future. However we hope the new merging algorithm additions will provide enough of an improvement to the generated models to make disabling of field merging unnecessary for most users.
To enable this option when using a json config file, use the configuration option experimentalFeatures.fieldMerging.
{
"experimentalFeatures" : {
"fieldMerging" : [
# Any combination of these values:
"siblings",
"ancestors",
"namedFragments"
],
"legacySafelistingCompatibleOperations" : true
},
"input": {
# ...
You may also input fieldMerging: [ "all" ], to enable all types of field merging (which is the default value if not provided).
ApolloCodegenLibTo enable this option when using the ApolloCodegenLib directly, set the ApolloCodegenConfiguration.experimentalFeatures.fieldMerging option.
config = ApolloCodegenConfiguration(
schemaNamespace: "MySchema,
input: // ...,
output: // ...,
options: // ... ,
experimentalFeatures: .init(
fieldMerging: [
# Any combination of these values:
.siblings,
.ancestors,
.namedFragments
]
)
)
You may also input fieldMerging: .all, to enable all types of field merging (which is the default value if not provided).
There is a longstanding bug (since Apollo iOS 1.0) in the codegen engine for some users that have large sets of operations with many deeply nested fragment spreads. For these users, the codegen engine never finishes running, using unbounded memory and eventually crashing once it has used all available memory. This version does not resolve this issue, but we are hoping to address this in a release in the near future!
Because the changes to the generated models in this version can be large in some circumstances, we would like to get feedback on any issues you encounter while using this preview version before we release this into a stable version of Apollo iOS. Please file an issue for any problems you encounter.
Issues may appear when using the new disabling of field merging, but we are also aware of possible issues when not using this new feature (ie. fieldMerging: .all)
We are particularly concerned about possible issues in the following situations:
@include/@skip conditionsIn addition to feedback on problems you encounter, we would also love to hear about your success stories! If this new version works well for you and reduces the size of your generated models in a meaningful way, please let us know in #2560!
Thank you for trying out this preview version of Apollo iOS. We appreciate your time and effort as well as any feedback you can provide.
GraphQLError is “too many validation errors”" (#438): When a GraphQLError from the JS parsing step is a “Too many validation errors” error, there is no source in the error object. Codegen will now check for it to avoid this edge case crash.@defer directive introduced a bug where the cache write interceptor would throw if no cache records were returned during response parsing. This is incorrect as there are no cache records in the case of an errors only GraphQL response.fatalError on JSONEncodable (#128): The fatal error logic in JSONEncodable was replaced with a type constraint where clause. Thank you to @arnauddorgans for the contribution.@defer directive definition (#3417): The codegen engine can now correctly detect pre-existing @defer directive definitions in introspection sources and prevent the duplicate definition. See PR #440. Thanks to @loganblevins for reporting the issue.@defer directive: You can now use the @defer directive in your operations and code generation will generate models that support asynchronously receiving the deferred selection sets. There is a helpful property wrapper with a projected value to determine the state of the deferred selection set, and support for cache reads and writes. This feature is enabled by default but is considered experimental. Please refer to the documentation for further details.debugDescription to SelectionSet (#3374): This adds the ability to easily print code generated models to the Xcode debugger console. See PR #412. Thanks to @raymondk-nf for raising the issue..editorconfig files that represent settings like spaces vs. tabs, how many spaces per tab, etc. We've added a .editorconfig file with the projects preferred settings, so that the editor will use them automatically. See PR #419. Thanks to @TizianoCoroneo for raising the issue.@apollo_client_ios_localCacheMutation directive caused a compile time error in Xcode 16 with Swift 6. See PR #417. Thanks to @martin-muller for raising the issue.ExistentialAny requirement (#379): This adds the -enable-upcoming-feature ExistentialAny to all targets to ensure compatibility with the upcoming Swift feature.SelectionSet instances to JSON.1.12.1 package was built with an incorrect version number causing a version mismatch when attempting to execute code generation.1.12.0 package was built with inconsistent SDK versions resulting in the linker signing not working correctly.