Subscriptions
GraphQL supports subscriptions to allow clients to be immediately updated when the data changes on a server.
The Apollo iOS library supports the use of subscriptions primarily through the use of ApolloWebSocket
, an optional additional library that uses popular iOS WebSocket library Starscream
under the hood to use WebSockets to connect to your GraphQL server.
Subscriptions are also supported through code generation: Any time your schema declares a subscription field, an operation conforming to GraphQLSubscription
will be generated which allows you to pass in any parameters that subscription field takes.
Once those operations are generated, you can use an instance of ApolloClient
using a subscription-supporting network transport to subscribe, and continue to receive updates about changes until the subscription is cancelled.
Transport types which support subscriptions
There are two different classes which conform to the NetworkTransport
protocol within the ApolloWebSocket
library:
WebSocketTransport
sends all operations over a web socket.SplitNetworkTransport
hangs onto both aWebSocketTransport
instance and anUploadingNetworkTransport
instance (usuallyRequestChainNetworkTransport
) in order to create a single network transport that can use http for queries and mutations, and web sockets for subscriptions.
Typically, you'll want to use SplitNetworkTransport
, since this allows you to retain the single NetworkTransport
setup and avoids any potential issues of using multiple client objects.
GraphQL over WebSocket protocols
There are two protocols supported by apollo-ios:
graphql-ws
protocol which is implemented in the subscriptions-transport-ws and AWS AppSync libraries.graphql-transport-ws
protocol which is implemented in the graphql-ws library.
It is important to note that the protocols are not cross-compatible and you will need to know which is implemented in the service you're connecting to. All WebSocket
initializers allow you to specify which GraphQL over WebSocket protocol should be used.
Sample subscription-supporting initializer
Here is an example of setting up a singleton similar to the Example Advanced Client Setup, but which uses a SplitNetworkTransport
to support both subscriptions and queries:
1import Foundation
2import Apollo
3import ApolloWebSocket
4
5// MARK: - Singleton Wrapper
6
7class Apollo {
8 static let shared = Apollo()
9
10 /// A web socket transport to use for subscriptions
11 private lazy var webSocketTransport: WebSocketTransport = {
12 let url = URL(string: "ws://localhost:8080/websocket")!
13 let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws)
14 return WebSocketTransport(websocket: webSocketClient)
15 }()
16
17 /// An HTTP transport to use for queries and mutations
18 private lazy var normalTransport: RequestChainNetworkTransport = {
19 let url = URL(string: "http://localhost:8080/graphql")!
20 return RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: self.store), endpointURL: url)
21 }()
22
23 /// A split network transport to allow the use of both of the above
24 /// transports through a single `NetworkTransport` instance.
25 private lazy var splitNetworkTransport = SplitNetworkTransport(
26 uploadingNetworkTransport: self.normalTransport,
27 webSocketNetworkTransport: self.webSocketTransport
28 )
29
30 /// Create a client using the `SplitNetworkTransport`.
31 private(set) lazy var client = ApolloClient(networkTransport: self.splitNetworkTransport, store: self.store)
32
33 /// A common store to use for `normalTransport` and `client`.
34 private lazy var store = ApolloStore()
35}
Example usage of a subscription
Let's say you're using the Sample Star Wars API, and you want to use a view controller with a UITableView
to show a list of reviews that will automatically update whenever a new review is added.
You can use the ReviewAddedSubscription
to accomplish this:
1class ReviewViewController: UIViewController {
2
3 private var subscription: Cancellable?
4 private var reviewList = [Review]()
5
6 // Assume data source and delegate are hooked up in Interface Builder
7 @IBOutlet private var reviewTableView: UITableView!
8
9 override func viewDidLoad() {
10 super.viewDidLoad()
11
12 // Set the subscription variable up - be careful not to create a retain cycle!
13 self.subscription = Apollo.shared.client
14 .subscribe(subscription: ReviewAddedSubscription()) { [weak self] result in
15 guard let self = self else {
16 return
17 }
18
19 switch result {
20 case .success(let graphQLResult):
21 if let review = graphQLResult.data?.reviewAdded {
22 // A review was added - append it to the list then reload the data.
23 self.reviewList.append(review)
24 self.reviewTableView.reloadData()
25 } // else, something went wrong and you should check `graphQLResult.error` for problems
26 case .failure(let error):
27 // Not included here: Show some kind of alert
28 }
29 }
30 }
31
32 deinit {
33 // Make sure the subscription is cancelled, if it exists, when this object is deallocated.
34 self.subscription?.cancel()
35 }
36
37 // MARK: - Standard TableView Stuff
38
39 func tableView(_ tableView: UITableView,
40 numberOfRowsInSection section: Int) -> Int {
41 return self.reviewList.count
42 }
43
44 func tableView(_ tableView: UITableView,
45 cellForRowAt indexPath: IndexPath) -> UITableViewCell {
46 // Assume `ReviewCell` is a cell for displaying reviews created elsewhere
47 guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? ReviewCell else {
48 return UITableViewCell()
49 }
50
51 let review = self.reviewList[indexPath.row]
52
53 cell.episode = review.episode
54 cell.stars = review.stars
55 cell.commentary = review.commentary
56
57 return cell
58 }
59}
Each time a review is added, the subscription's closure is called and if the proper data is included, the new data will be displayed immediately.
Note that if you only wanted to be updated reviews for a specific episode, you could specify that episode in the initializer for ReviewAddedSubscription
.
Subscriptions and authorization tokens
In a standard HTTP operation, if authentication is necessary an Authorization
header is often sent with requests. However, with a web socket, this can't be sent with every payload since a persistent connection is required.
For web sockets, the connectingPayload
provides those parameters you would traditionally specify as part of the headers of your request.
Note that this must be set when the WebSocketTransport
is created. If you need to update the connectingPayload
, you will need to recreate the client using a new webSocketTransport
.
Assuming you (or your backend developers) have read the authentication section and subscriptions example / authentication over WebSocket of our backend documentation, you will need to initialize your ApolloClient
instance as follows:
1import Foundation
2import Apollo
3import ApolloWebSocket
4
5// MARK: - Singleton Wrapper
6
7let magicToken = "So long and thanks for all the fish"
8
9class Apollo {
10 static let shared = Apollo()
11
12 /// A web socket transport to use for subscriptions
13 // This web socket will have to provide the connecting payload which
14 // initializes the connection as an authorized channel.
15 private lazy var webSocketTransport: WebSocketTransport = {
16 let url = URL(string: "ws://localhost:8080/websocket")!
17 let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws)
18 let authPayload = ["authToken": magicToken]
19 return WebSocketTransport(websocket: webSocketClient, connectingPayload: authPayload)
20 }()
21
22 /// An HTTP transport to use for queries and mutations.
23 private lazy var normalTransport: RequestChainNetworkTransport = {
24 let url = URL(string: "http://localhost:8080/graphql")!
25 return RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: self.store), endpointURL: url)
26 }()
27
28 /// A split network transport to allow the use of both of the above
29 /// transports through a single `NetworkTransport` instance.
30 private lazy var splitNetworkTransport = SplitNetworkTransport(
31 uploadingNetworkTransport: self.normalTransport,
32 webSocketNetworkTransport: self.webSocketTransport
33 )
34
35 /// Create a client using the `SplitNetworkTransport`.
36 private(set) lazy var client = ApolloClient(networkTransport: self.splitNetworkTransport, store: self.store)
37
38 /// A common store to use for `normalTransport` and `client`.
39 private lazy var store = ApolloStore()
40}