Creating a client
Basic Client Creation
In most cases, you'll want to create a single shared instance of ApolloClient
and point it at your GraphQL server. The easiest way to do this is to create a singleton:
class Network {
static let shared = Network()
private(set) lazy var apollo = ApolloClient(url: URL(string: "http://localhost:8080/graphql")!)
}
Under the hood, this will create a client using HTTPNetworkTransport
with a default configuration. You can then use this client from anywhere in your code with Network.shared.apollo
.
Advanced Client Creation
For more advanced usage of the client, you can use this initializer which allows you to pass in an object conforming to the NetworkTransport
protocol, as well as a store if you wish:
public init(networkTransport: NetworkTransport,
store: ApolloStore = ApolloStore(cache: InMemoryNormalizedCache()))
The available implementations are:
RequestChainNetworkTransport
, which passes a request through a chain of interceptors that can do work both before and after going to the network, and uses standard HTTP requests to communicate with the serverWebSocketTransport
, which will send everything using a web socket. If you're using CocoaPods, make sure to install theApollo/WebSocket
sub-spec to access this.SplitNetworkTransport
, which will send subscription operations via a web socket and all other operations via HTTP. If you're using CocoaPods, make sure to install theApollo/WebSocket
sub-spec to access this.
Using RequestChainNetworkTransport
The initializer for RequestChainNetworkTransport
has several properties which can allow you to get better information and finer-grained control of your HTTP requests and responses:
interceptorProvider
: The interceptor provider to use when constructing chains for a request. See below for details on interceptor providers.endpointURL
: The GraphQL endpoint URL to use for all calls.additionalHeaders
: Any additional headers that should be automatically added to every request. Defaults to an empty dictionary.autoPersistQueries
: Passtrue
if Automatic Persisted Queries should be used to send an operation's hash instead of the full operation body by default. NOTE: To use APQs, you need to make sure to generate your types with operation identifiers. In your Swift Script, make sure to pass a non-niloperationIDsURL
to have this output. Due to this restriction, this option defaults tofalse
.requestCreator
: TheRequestCreator
object to use to build yourURLRequest
. Defaults to the providedApolloRequestCreator
implementation.useGETForQueries
: Sends all requests ofquery
andmutation
types usingGET
instead ofPOST
. This is mostly useful for large companies taking advantage of CDNs (Content Distribution Networks) that allow local caches instead of going all the way to your server for data which does not change often. This defaults tofalse
to preserve existing behavior in older versions of the client.useGETForPersistedQueryRetry
: Passtrue
to useGET
instead ofPOST
for a retry of a persisted query. Defaults tofalse
.
How the RequestChain
works
A RequestChain
is constructed using an array of interceptors, to be run in the order given, and handles calling back on a specified DispatchQueue
after all work is complete.
In each interceptor, work can be performed asynchronously on any thread. To move along to the next interceptor in the chain, call proceedAsync
.
By default, when the interceptor chain ends, if you have a parsed result available, this result will be returned to the caller.
If you want to directly return a value to the caller, call returnValueAsync
. If you want to have the chain return an error, call handleErrorAsync
. Both of these methods will call your completion block on the queue specified when creating the `RequestChain.
Note that calling returnValue
does NOT forbid calling handleError
- or calling each more than once. For example, if you want to return data from the cache to the UI while a network fetch executes, you'd want to make sure that returnValueAsync
was called twice.
The chain also includes a retry
mechanism, which will go all the way back to the first interceptor in the chain, then start running through the interceptors again.
IMPORTANT: Do not call retry
blindly. If your server is returning 500s or if the user has no internet, this will create an infinite loop of requests that are retrying (especially if you're not using something like the MaxRetryInterceptor
to limit how many retries are made). This will kill your user's battery, and might also run up the bill on their data plan. Make sure to only request a retry when there's something your code can actually do about the problem!
In the RequestChainNetworkTransport
, each request creates an individual request chain, and uses an InterceptorProvider
Setting up ApolloInterceptor
chains with InterceptorProvider
Every operation sent through a RequestChainNetworkTransport
will be passed into an InterceptorProvider
before going to the network. This protocol creates an array of interceptors for use by a single request chain based on the provided operation.
There are two default implementations for this protocol provided:
LegacyInterceptorProvider
works with our existing parsing and caching system and tries to replicate the experience of using the oldHTTPNetworkTransport
as closely as possible. It takes aURLSessionClient
and anApolloStore
to pass into the interceptors it uses.CodableInterceptorProvider
is a work in progress, which is going to be for use with our Swift Codegen Rewrite, (which, I swear, will eventually be finished). It is not suitable for use at this time. It takes aURLSessionClient
, aFlexibleDecoder
(something can decode anything that conforms toDecodable
). It does not support caching yet.
If you wish to make your own InterceptorProvider
instead of using the provided one, you can take advantage of several interceptors that are included in the library:
Pre-network
MaxRetryInterceptor
checks to make sure a query has not been tried more than a maximum number of times.LegacyCacheReadInterceptor
reads from a providedApolloStore
based on thecachePolicy
, and will return a resul if one is found.
Network
NetworkFetchInterceptor
takes aURLSessionClient
and uses it to send the preparedHTTPRequest
(or subclass thereof) to the server.
Post-Network
ResponseCodeInterceptor
checks to make sure a valid response status code has been returned. NOTE: Most errors at the GraphQL level are returned with a200
status code and information in theerrors
array per the GraphQL Spec. This interceptor helps with things like server errors and errors that are returned by middleware. This article on error handling in GraphQL is a really helpful look at how and why these differences occur.AutomaticPersistedQueryInterceptor
handles checking responses to see if an error is because an automatic persisted query failed, and the full operation needs to be resent to the server.LegacyParsingInterceptor
parses code generated by our Typescript code generation.LegacyCacheWriteInterceptor
writes to a providedApolloStore
.CodableParsingError
is a work in progress which will parseCodable
results form the Swift Codegen Rewrite.
The URLSessionClient class
Since URLSession
only supports use in the background using the delegate-based API, we have created our own URLSessionClient
which handles the basics of setup for that.
One thing to be aware of: Because setting up a delegate is only possible in the initializer for URLSession
, you can only pass in a URLSessionConfiguration
, not an existing URLSession
, to this class's initializer.
By default, instances of URLSessionClient
use URLSessionConfiguration.default
to set up their URL session, and instances of HTTPNetworkTransport
use the default initializer for URLSessionClient
.
The URLSessionClient
class and most of its methods are open
so you can subclass it if you need to override any of the delegate methods for the URLSession
delegates we're using or you need to handle additional delegate scenarios.
Example Advanced Client Setup
Here's a sample how to use an advanced client with some custom interceptors. This code assumes you've got the following classes in your own code (these are not part of the Apollo library):
UserManager
to check whether the user is logged in, perform associated checks on errors and responses to see if they need to renew their token, and perform that renewalLogger
to handle printing logs based on their level, and which supports.debug
,.error
, or.always
log levels.
Example interceptors
Sample UserManagementInteceptor
An interceptor which checks if the user is logged in and then renews the user's token if it is expired asynchronously before continuing the chain, using the above-mentioned UserManager
class:
class UserManagementInterceptor: ApolloInterceptor {
enum UserError: Error {
case noUserLoggedIn
}
/// Helper function to add the token then move on to the next step
private func addTokenAndProceed<Operation: GraphQLOperation>(
_ token: Token,
to request: HTTPRequest<Operation>,
chain: RequestChain,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
request.addHeader(name: "Authentication", value: "Bearer: \(token.value)")
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
guard let token = UserManager.shared.token else {
// In this instance, no user is logged in, so we want to call
// the error handler, then return to prevent further work
chain.handleErrorAsync(UserError.noUserLoggedIn,
request: request,
response: response,
completion: completion)
return
}
// If we've gotten here, there is a token!
if token.isExpired {
// Call an async method to renew the token
UserManager.shared.renewToken { [weak self] tokenRenewResult in
guard let self = self else {
return
}
switch tokenRenewResult {
case .failure(let error):
// Pass the token renewal error up the chain, and do
// not proceed further. Note that you could also wrap this in a
// `UserError` if you want.
chain.handleErrorAsync(error,
request: request,
response: response,
completion: completion)
case .success(let token):
// Renewing worked! Add the token and move on
self.addTokenAndProceed(token,
to: request,
chain: chain,
response: response,
completion: completion)
}
}
} else {
// We don't need to wait for renewal, add token and move on
self.addTokenAndProceed(token,
to: request,
chain: chain,
response: response,
completion: completion)
}
}
}
Sample RequestLoggingInterceptor
An interceptor which logs the outgoing request using the above-mentioned Logger
class, then moves on:
class RequestLoggingInterceptor: ApolloInterceptor {
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
Logger.log(.debug, "Outgoing request: \(request)")
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
}
Sample ‌ResponseLoggingInterceptor
An interceptor using the above-mentioned Logger
which logs the incoming response if it exists, and moves on.
Note that this is an example of an interceptor which can both proceed and throw an error - we don't necessarily want to stop processing if this was set up in the wrong place, but we do want to know about it.
class ResponseLoggingInterceptor: ApolloInterceptor {
enum ResponseLoggingError: Error {
case notYetReceived
}
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {
defer {
// Even if we can't log, we still want to keep going.
chain.proceedAsync(request: request,
response: response,
completion: completion)
}
guard let receivedResponse = response else {
chain.handleErrorAsync(ResponseLoggingError.notYetReceived,
request: request,
response: response,
completion: completion)
return
}
Logger.log(.debug, "HTTP Response: \(receivedResponse.httpResponse)")
if let stringData = String(bytes: receivedResponse.rawData, encoding: .utf8) {
Logger.log(.debug, "Data: \(stringData)")
} else {
Logger.log(.error, "Could not convert data to string!")
}
}
}
Example Custom Interceptor Provider
This InterceptorProvider
uses all of the interceptors that (as of this writing) are in the default LegacyInterceptorProvider
, interspersed at the appropriate points with the sample interceptors created above:
struct NetworkInterceptorProvider: InterceptorProvider {
// These properties will remain the same throughout the life of the `InterceptorProvider`, even though they
// will be handed to different interceptors.
private let store: ApolloStore
private let client: URLSessionClient
init(store: ApolloStore,
client: URLSessionClient) {
self.store = store
self.client = client
}
func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
LegacyCacheReadInterceptor(store: self.store),
TokenAddingInterceptor(),
RequestLoggingInterceptor(),
NetworkFetchInterceptor(client: self.client),
ResponseLoggingInterceptor(),
ResponseCodeInterceptor(),
LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
LegacyCacheWriteInterceptor(store: self.store)
]
}
}
Example Network Singleton Setup
This is the equivalent of what you'd set up in the Basic Client Creation section, and what you'd call into from your application.
class Network {
static let shared = Network()
private(set) lazy var apollo: ApolloClient = {
// The cache is necessary to set up the store, which we're going to hand to the provider
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let client = URLSessionClient()
let provider = NetworkInterceptorProvider(store: store, client: client)
let url = URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")!
let requestChainTransport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url)
// Remember to give the store you already created to the client so it
// doesn't create one on its own
return ApolloClient(networkTransport: requestChainTransport,
store: store)
}()
}
An example of setting up a client which can handle web sockets and subscriptions is included in the subscription documentation.