Unleashing the power of GraphQL in your iOS app
Streamline data fetching in your iOS app using GraphQL and Apollo Framework together with SwiftUI and MVVM architecture
Introduction
GraphQL has revolutionized the way we fetch and manage data in modern development. Unlike traditional REST APIs, GraphQL allows us to retrieve precisely the data we need with a single request, eliminating the need for multiple round trips. With its powerful query language, consumers of the API can specify the exact data they require, resulting in more efficient and flexible data fetching.
Since its open-source release in 2015, GraphQL has gained immense popularity and has fostered a vibrant community around it. Within this community, several tools and libraries have emerged to enhance the GraphQL ecosystem. One such tool is Apollo, a feature-rich and type-safe caching implementation of GraphQL.
In this tutorial, we will explore the capabilities of GraphQL and Apollo by utilizing the ChargeTrip Playground API as our testing ground. You can find the final code in this GitHub repository. We will dive into GraphQL queries, leveraging the expressive power of the language, and demonstrate how Apollo simplifies data fetching and caching in GraphQL-based applications. So let’s get started and unlock the full potential of GraphQL with Apollo!
Add the Apollo iOS SDK to your project
To begin using the Apollo iOS SDK in your project, follow these steps:
- Launch Xcode and open your project. Then, navigate to File > Add Packages…. This will open the Add Package dialog.
- By default, the Add Package dialog displays Apple packages. However, you need to add the Apollo iOS package. In the upper right-hand corner of the dialog, paste the following URL into the search bar: https://github.com/apollographql/apollo-ios.
3. Xcode will now show you the apollo-ios
package in the search results. In the right-hand panel, you can select the version you want to use. For this tutorial, I recommend selecting Up to Next Minor from the Version dropdown. Note that there may be minor breaking changes between minor releases, so it's important to choose a version that suits your project's requirements. At the time of writing, the current minor version is 1.3.x.
4. After selecting the version, click Add Package. Xcode’s Swift Package Manager (SPM) will start checking out the package and integrating it into your project. Once the process is complete, you will see a list of framework targets included in the apollo-ios
library.
5. In the list of framework targets, select only the main Apollo target. This ensures that only the necessary components are added to your project.
6. Finally, click Add Package to confirm your selection. SPM will fetch the necessary dependencies for the apollo-ios
package. Once the fetch process is complete, you will be able to see the added dependencies in the project navigator of Xcode.
Setup Codegen CLI
The Codegen CLI is an essential tool provided by the Apollo iOS SPM package. It allows you to generate Swift code from your GraphQL schema and queries. To set up the Codegen CLI, follow the instructions below:
- In your Xcode project, locate the project file in the file explorer. Right-click on the project file to reveal a context menu.
- From the context menu, select the
Install CLI
plugin command. This will initiate the installation process for the Codegen CLI.
3. During the installation, a dialog may appear, requesting write access to your project directory. Grant the plugin “write” access to proceed with the installation.
4. Once the installation is complete, a symbolic link named apollo-ios-cli
will be created in your project's root folder. This link points to the actual executable of the Codegen CLI.
5. To run the Codegen CLI, open the terminal and navigate to your project’s root folder. Execute the following command: ./apollo-ios-cli
. This will execute the Codegen CLI and allow you to generate Swift code based on your GraphQL schema and queries.
Note: The apollo-ios-cli
symbolic link works only if the compiled CLI executable exists. In most cases, this executable is located in the Xcode Derived Data or .build
folder. If you clear these folders or encounter issues, you can rerun the Install CLI
plugin to rebuild the CLI executable.
⚠️ Important: There is a known bug in Xcode 14.3 where the
Install CLI
plugin command may not appear in the right-click menu for your project. This issue is being tracked here. If you encounter this problem, you can either try using a different version of Xcode or manually download a pre-built binary of the CLI. As described below.
For each release of Apollo iOS on GitHub, there is a pre-built CLI binary available for download. You can find these binaries in the releases section. After downloading the binary, you can move it to a convenient local directory. Make sure to run the Codegen CLI from the directory where it is located. A suggested approach is to place it in your project directory.
When you double-click on the apollo-ios-cli
binary file, macOS may display an error regarding the unidentified developer and potential malware.
To resolve this issue, right-click on the binary file and select “Open.” The system will prompt you to confirm that you want to open the file. Click “Open” again to indicate that the file is safe to use.
Once the terminal window opens, close it, and continue following this guide to proceed with the setup process.
Create your Codegen Configuration
To begin, let’s set up the codegen configuration file for our project. Follow the steps below:
- Open the Terminal and navigate to your project directory (or the directory where the
apollo-ios-cli
is located). You can do this by typingcd
followed by a space in the Terminal and then dragging the project directory into the Terminal window. Press Enter to navigate to the specified directory. - In the Terminal, run the following command:
./apollo-ios-cli init --schema-namespace ChargeTripAPI --module-type swiftPackageManager
This command generates a basic apollo-codegen-config.json
file specifically tailored to our project.
Download your server’s schema
Next, we need to download the schema for our project. To do this, we’ll update the apollo-codegen-config.json
file to include a schemaDownloadConfiguration
. Follow the steps below:
- Open the
apollo-codegen-config.json
file in a text editor. - Locate the
output
object within the file. After theoutput
object, add the following JSON code:
"schemaDownloadConfiguration": {
"downloadMethod": {
"introspection": {
"endpointURL": "https://api.chargetrip.io/graphql",
"httpMethod": {
"POST": {}
},
"includeDeprecatedInputValues": false,
"outputFormat": "SDL"
}
},
"downloadTimeout": 60,
"headers": [],
"outputPath": "./graphql/schema.graphqls"
}
This configuration specifies the download method for the schema, including the endpoint URL, HTTP method, and output format. Adjust the values as needed to match your project’s requirements. Save the changes to the apollo-codegen-config.json
file.
Now that we’ve updated the configuration file, we can proceed to download the schema. In the Terminal, run the following command:
./apollo-ios-cli fetch-schema
This command will initiate the schema download process using the configured settings.
After running the command, you should see a graphql
folder in your project directory. Inside this folder, you'll find a schema.graphqls
file. This file contains the downloaded schema from your server.
Write your first query
The most common operation in GraphQL is the query, which allows you to request specific data from your GraphQL server based on its schema.
In the docs section that you can open by pressing the label on the right side of the ChargeTrip Playground, you can see both the query term itself, the return type, and information about parameters that can be passed to the query. We will use that information to write our query.
Now we can start by writing a simple query that retrieves a list of stations. In this example, we’ll request the id
, address
, city
, and name
of the station owner
for each station. Here's the query:
query StationList {
stationList {
id
address
city
owner {
name
}
}
}
Let’s break down the structure of this query:
- The outermost brackets define the query operation. In this case, it’s a
query
operation namedStationList
. - Inside the query operation, we have the
selection set
enclosed in another set of brackets. The selection set specifies the fields we want to retrieve from the server. - Within the selection set, we specify the fields we want to fetch for each
station
object, includingid
,address
,city
, and thename
of the station'sowner
.
The Apollo iOS SDK requires every query to have a name, even though it’s not strictly required by the GraphQL specification. Naming the query operation helps in distinguishing and organizing multiple queries within your app.
To test this query, you can use the ChargeTrip Playground and click the “Play” button in the center of the page. The query will be executed, and you’ll see the results as a JSON object displayed on the right-hand side of the page.
Add the query to Xcode
To use the GraphQL query in your Xcode project, follow the steps below:
- In Xcode, locate the
graphql
folder in your project hierarchy. Right-click on thegraphql
folder and select New File... from the context menu. - In the New File dialog, navigate to the Other section and select the Empty file template. Click Next to proceed.
- In the next dialog, name the file
StationList.graphql
. Make sure the file is being added to thegraphql
folder where yourschema.graphqls
file is located. It's important to ensure that the file is not added to the app target. Click Create to create the file.
4. Open the newly created StationList.graphql
file and paste the following query into it:
query StationList {
stationList {
id
address
city
owner {
name
}
}
}
Running code generation
Now that we have both the schema and query files in place, we can run the code generation process. To generate the code, follow these steps:
- Open the Terminal and navigate to your project directory.
- Run the following command in the Terminal:
./apollo-ios-cli generate
This command will trigger the code generation process using the downloaded schema and query files. As a result, you will see a new folder named ChargeTripAPI
in your project directory. This folder contains the Swift package with the generated source code.
Add the generated SPM package to your project
Once the code generation is complete, we need to add the generated Swift Package Manager (SPM) package to our Xcode project. Follow these steps:
- In Xcode, go to File > Add Packages….
- In the Add Package dialog, select Add Local….
- Navigate to the
ChargeTripAPI
folder in the file dialog and click Add Package. - The
ChargeTripAPI
package will now be included in your project. - Next, select your project in Xcode, then select the
ChargeTrip GraphQL
target, and navigate to Build Phases. - Under the Link Binary With Libraries section, click the + sign.
- In the dialog that appears, select the
ChargeTripAPI
library and click Add Package....
Now the generated SPM package is added to your Xcode project, allowing you to use the generated code in your app.
Execute your first query
To execute your first query using the generated code, you need to create an instance of ApolloClient
. This instance will use the generated code to make network calls to your server. It's recommended to create a singleton or static instance of ApolloClient
that can be accessed from anywhere in your codebase.
Follow these steps to create the ApolloClient
instance:
- Create a new Swift file within the
ChargeTrip GraphQL
group in Xcode. Name the fileNetwork.swift
and set the target toChargeTrip GraphQL
. - Import the
Apollo
module at the top of the file. - Create a
Network
class that holds theApolloClient
instance. Your file should look like this:
import Foundation
import Apollo
import Foundation
import Apollo
class Network {
static let shared = Network()
private init() {}
private(set) lazy var apollo = ApolloClient(url: URL(string: "https://api.chargetrip.io/graphql")!)
}
The Network
class uses the singleton pattern to ensure that there is only one instance of ApolloClient
throughout your app. The apollo
property represents the ApolloClient
instance, and it's initialized with the URL of your GraphQL server.
Implement the query
To implement the query and communicate with the server using your ApolloClient
instance, follow these steps:
- Create a new Swift file called
StationListViewModel
and importApollo
andChargeTripAPI
. This file will contain the implementation of your ViewModel. - In the
StationListViewModel
, create a class namedStationListViewModel
that conforms to theObservableObject
protocol. This allows the ViewModel to publish changes to its properties. - Inside the
StationListViewModel
, add an initializer. In the initializer, use theNetwork.shared.apollo
instance to fetch theStationListQuery
from the server. Handle the result using a closure.
import Foundation
import Apollo
import ChargeTripAPI
@MainActor
final class StationListViewModel: ObservableObject {
init() {
Network.shared.apollo.fetch(query: StationListQuery()) { result in
switch result {
case .success(let graphQLResult):
print("Success! Result: \\(graphQLResult)")
case .failure(let error):
print("Failure! Error: \\(error)")
}
}
}
}
4. Create a new SwiftUI view called StationListView
(instead of the default ContentView
). Remember to replace also the ContentView()
in the ChargeTrip_GraphQLApp
file with the StationListView()
.
5. Inside the StationListView
, add a @StateObject
property wrapper to create an instance of StationListViewModel
as the ViewModel for this view.
struct StationListView: View {
@StateObject var viewModel = StationListViewModel()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
struct StationListView_Previews: PreviewProvider {
static var previews: some View {
StationListView()
}
}
Now you have implemented the query and created a SwiftUI view that uses the StationListViewModel
. The query will be executed when the StationListViewModel
is initialized, and the result will be printed in the console.
Test your query
To test your query, run the app on the simulator. You will notice that errors are printed in the console. This is because you need to authenticate before making requests to the server.
Authenticate your operations
To authenticate your operations with the server, you’ll need to add an interceptor to your ApolloClient
instance. This interceptor will add the necessary headers, such as the client ID and app ID, to authenticate the requests.
Let’s delve deeper into the inner workings of ApolloClient
in iOS.
The functioning of ApolloClient
relies on a component known as NetworkTransport
. By default, the client generates an instance of RequestChainNetworkTransport
to handle communication with the server via HTTP.
A RequestChain
processes your request through an array of ApolloInterceptor
objects. These interceptors have the ability to modify the request, check the cache before sending it to the network, and perform additional tasks after receiving a response from the network.
To construct the array of interceptors for each operation executed by the RequestChainNetworkTransport
, an object conforming to the InterceptorProvider
protocol is utilized. By default, there are a few providers established that return a standard array of interceptors.
The advantage here is that you can also include your own interceptors at any point in the chain to perform custom actions. In this scenario, you want to create an interceptor that adds your keys.
- To begin, create a Model to incorporate your keys. Generate a new Swift file called
AuthorizationKeys
and implement the following struct:
import Foundation
struct AuthorizationKeys {
let clientID: XClientID
let appID: XAppID
init(clientID: String, appID: String){
self.clientID = .init(value: clientID)
self.appID = .init(value: appID)
}
struct XClientID {
let name: String = "x-client-id"
let value: String
}
struct XAppID {
let name: String = "x-app-id"
let value: String
}
}
2. Create a new Swift file called AuthorizationInterceptor
. This file will contain the implementation of the interceptor. Paste the following code into the file:
import Foundation
import Apollo
class AuthorizationInterceptor: ApolloInterceptor {
// Any custom interceptors you use are required to be able to identify themselves through an id property.
public var id: String = UUID().uuidString
func interceptAsync<Operation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) where Operation : GraphQLOperation {
let keys = AuthorizationKeys(
clientID: "YOUR_CLIENT_ID",
appID: "YOUR_APP_ID"
)
request.addHeader(name: keys.clientID.name, value: keys.clientID.value)
request.addHeader(name: keys.appID.name, value: keys.appID.value)
chain.proceedAsync(request: request,
response: response,
interceptor: self,
completion: completion)
}
}
3. Create a new Swift file called NetworkInterceptorProvider
. This file will subclass the DefaultInterceptorProvider
and insert the AuthorizationInterceptor
at the beginning of the interceptors array. Paste the following code into the file:
import Foundation
import Apollo
class NetworkInterceptorProvider: DefaultInterceptorProvider {
override func interceptors<Operation>(for operation: Operation) -> [ApolloInterceptor] where Operation : GraphQLOperation {
var interceptors = super.interceptors(for: operation)
interceptors.insert(AuthorizationInterceptor(), at: 0)
return interceptors
}
}
An alternative approach could involve duplicating and inserting the list of interceptors presented by the
DefaultInterceptorProvider
(which are all accessible), and subsequently positioning your interceptors within the array at the desired locations. Nevertheless, given that we can execute this interceptor as the initial step in this scenario, it is more straightforward to create a subclass.
Get your keys
To get your keys you need to register at the https://account.chargetrip.com/sign-up and then create a new project. Here you need to choose a name and then press Next until you reach the Create button.
Here you can now get your keys and replace them in the keys property:
let keys = AuthorizationKeys(
clientID: "YOUR_CLIENT_ID",
appID: "YOUR_APP_ID"
)
Update the Network class to use the interceptor
To update the Network
class to use the authentication interceptor, follow these steps:
- Open the
Network.swift
file. - Update the
apollo
property of theNetwork
class to use theApolloClient
initializer that takes anInterceptorProvider
argument. Replace the existingapollo
property with the following code:
private(set) lazy var apollo: ApolloClient = {
let networkTransport = RequestChainNetworkTransport(
interceptorProvider: NetworkInterceptorProvider(),
endpointURL: URL(string: "https://api.chargetrip.io/graphql")!
)
return ApolloClient(networkTransport: networkTransport)
}()
The ApolloClient
is now initialized with the RequestChainNetworkTransport
that uses the NetworkInterceptorProvider
as the interceptorProvider
. This ensures that the authentication interceptor is applied to the network requests.
By incorporating the authentication interceptor, your app will now send authenticated requests to the server. Now you can run your app and you will see the station list printed in the console.
Adjust the ViewModel to our needs
Now, we need to create some functions to build our UI and the loadStations()
method:
import Foundation
import Apollo
import ChargeTripAPI
@MainActor
final class StationListViewModel: ObservableObject {
@Published var stationList: [StationListQuery.Data.StationList] = []
func loadStations() {
Network.shared.apollo.fetch(query: StationListQuery()) { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let graphQLResult):
// check the `data` property
if let stationConnection = graphQLResult.data?.stationList {
self.stationList.append(contentsOf: stationConnection.compactMap({ $0 }))
}
if let errors = graphQLResult.errors {
print(errors)
}
case .failure(let error):
print(error)
}
}
}
func address(of station: StationListQuery.Data.StationList) -> String {
var fullAddress = ""
if let address = station.address {
fullAddress.append(address)
}
if let city = station.city {
fullAddress.append(", \\(city)")
}
return fullAddress
}
func ownerName(of station: StationListQuery.Data.StationList) -> String {
station.owner?.name ?? "Unknowed owner"
}
}
In the .success
case, you will receive a GraphQLResult
. It has both a data
attribute and an errors
attribute. This is due to GraphQL permitting the retrieval of partial data if it is not null.
Therefore, when you receive a GraphQLResult
, it is advisable to examine both the data
attribute (to present any obtained results from the server) and the errors
attribute (to address any errors received from the server).
Implement the UI
Next, let's implement a simple UI with a .task
modifier to load the stations:
import SwiftUI
struct StationListView: View {
// ViewModel instance
@StateObject var viewModel = StationListViewModel()
var body: some View {
VStack(alignment: .leading){
List(viewModel.stationList, id: \\.self){ station in
Section {
Text("Owner name: " + viewModel.ownerName(of: station))
.fontWeight(.bold)
Text("Address: " + viewModel.address(of: station))
.fontWeight(.light)
}
}
}
.task {
viewModel.loadStations()
}
}
}
Conclusion
In this tutorial, we explored how to integrate GraphQL into an iOS app using the Apollo iOS SDK. We covered the steps required to set up the Apollo iOS SDK, authenticate requests, write GraphQL queries, and display the retrieved data in a SwiftUI user interface.
You now have the knowledge to build more complex GraphQL-powered apps and leverage the power of GraphQL to efficiently fetch and manipulate data from a server.
Thanks for reading! If you want to talk or take a coffee, contact me at vincenz.pascarella@gmail.com or connect via LinkedIn.