Complete GraphQL Guide
Table of Contents
- Chapter 1: What is GraphQL?
- Chapter 2: GraphQL vs REST — A Detailed Comparison
- Chapter 3: Understanding the Countries GraphQL API
- Chapter 4: Setting Up Your Xcode Project
- Chapter 5: Installing Apollo iOS via Swift Package Manager
- Chapter 6: Downloading the GraphQL Schema
- Chapter 7: Writing Your First GraphQL Query
- Chapter 8: Running Code Generation
- Chapter 9: Building the Network Layer
- Chapter 10: Fetching Data & Displaying in SwiftUI
- Chapter 11: Using Variables and Arguments
- Chapter 12: Using Fragments for Reusable Fields
- Chapter 13: Error Handling
- Chapter 14: Understanding the Apollo Cache
- Chapter 15: Building the Complete Country Explorer App
- Chapter 16: Where to Go From Here
Chapter 1: What is GraphQL?
1.1 — The Big Picture
Imagine you are ordering food at a restaurant. With a traditional approach, you get a fixed meal — maybe a burger, fries, and a drink — whether you wanted the fries or not. You have no control over what comes on the plate. That is how REST APIs work: you call an endpoint, and the server decides exactly what data to send you back.
Now imagine a restaurant where you write your own order from scratch. You say: "I want the burger, hold the fries, add extra pickles, and give me a milkshake instead of water." The kitchen gives you exactly what you asked for — nothing more, nothing less. That is GraphQL.
GraphQL (Graph Query Language) is a query language for APIs invented by Facebook in 2012 and open-sourced in 2015. Instead of the server deciding what shape the data takes, the client decides. You write a query describing exactly the data you need, send it to a single endpoint, and receive a JSON response that mirrors the shape of your query.
1.2 — The Three Pillars of GraphQL
Queries — Reading Data
A query is how you ask a GraphQL server for data. Think of it as a GET request, but far more flexible. Here is a simple query that asks for a country's name and capital:
query {
country(code: "CA") {
name
capital
}
}
And the server responds with:
{
"data": {
"country": {
"name": "Canada",
"capital": "Ottawa"
}
}
}
Notice how the response shape mirrors the query shape exactly. This is one of GraphQL's most powerful features — you always know what you are going to get back.
Mutations — Writing Data
Mutations are how you create, update, or delete data. They are like POST, PUT, or DELETE requests in REST. The Countries API we are using is read-only, so we will not use mutations in this tutorial, but here is what one looks like conceptually:
mutation {
createUser(name: "Alice", email: "alice@example.com") {
id
name
}
}
Subscriptions — Real-Time Data
Subscriptions let you listen for real-time updates over a WebSocket connection. When the data on the server changes, your client gets notified automatically. We will not use subscriptions in this tutorial, but they are a powerful third pillar of GraphQL.
1.3 — Key Vocabulary
| Term | What It Means | Analogy |
|---|---|---|
| Schema | The contract defining all available types and fields | A restaurant menu |
| Query | A request for specific data | Your food order |
| Mutation | A request to change data | Asking the chef to modify a dish |
| Resolver | Server function that fetches data for a field | The chef cooking your order |
| Type | A shape of data (like a Swift struct) | A category on the menu |
| Field | A single piece of data on a type | An item on the menu |
| Argument | A parameter passed to a field | "Extra pickles, no onion" |
| Fragment | A reusable set of fields | A combo meal you can add to any order |
Chapter 2: GraphQL vs REST — A Detailed Comparison
If you have ever built an iOS app, you have almost certainly used REST APIs. Understanding how GraphQL differs from REST will help you appreciate why GraphQL exists and when to use it.
2.1 — The Core Differences
Endpoints
With REST, each resource typically lives at its own URL. You might have /api/countries, /api/countries/CA, /api/countries/CA/languages, and so on. Each endpoint returns a fixed structure.
With GraphQL, there is one single endpoint (for example, https://countries.trevorblades.com). Every request goes to the same URL. The query you send in the request body determines what data you get back.
Over-Fetching and Under-Fetching
Over-fetching means getting more data than you need. A REST endpoint for a country might return 30 fields when you only need the name and capital. You are downloading and parsing data you will never use, wasting bandwidth and battery.
Under-fetching means not getting enough data in one request. To show a country with its languages and continent name, you might need three separate REST calls: one for the country, one for its languages, and one for the continent. Each call adds latency.
GraphQL solves both problems. You specify exactly what fields you need, and you get everything in a single request.
2.2 — Side-by-Side Comparison
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Many (one per resource) | One (single endpoint) |
| Data shape | Decided by server | Decided by client |
| Over-fetching | Common problem | Eliminated by design |
| Under-fetching | Requires multiple calls | Single query gets all data |
| Versioning | Often /v1/, /v2/ |
Schema evolves, no versioning needed |
| Tooling | Postman, curl | GraphQL Playground, Apollo Studio |
| Type safety | Requires Swagger/OpenAPI | Built into the schema |
| Learning curve | Lower (familiar) | Moderate (new syntax) |
| Caching | HTTP caching is standard | Requires a smart client (Apollo) |
2.3 — When to Use GraphQL
GraphQL is not a universal replacement for REST. It excels in specific scenarios:
- Mobile apps that need to minimize data transfer over cellular networks
- Apps that display data from many related types on one screen
- Rapidly evolving APIs where frontend and backend teams move independently
- Situations where multiple client platforms (iOS, Android, web) need different data shapes
REST remains a great choice for simple CRUD apps, file uploads, and scenarios where HTTP caching is critical.
Chapter 3: Understanding the Countries GraphQL API
For this tutorial, we will use a free, public, no-authentication-required GraphQL API that provides information about countries, continents, and languages. This makes it perfect for learning — no API keys, no sign-up, no rate limits to worry about.
3.1 — The Endpoint
The API lives at a single URL: https://countries.trevorblades.com. Every GraphQL request you send will go to this one address.
3.2 — The Schema — What Data Is Available?
The Countries API exposes these main root query fields:
countries— Returns a list of all countries. Each country has fields like code, name, native name, capital, emoji flag, currency, languages, continent, states, and more.country(code: ID!)— Returns a single country by its two-letter ISO code (like"CA"for Canada,"US"for the United States,"JP"for Japan).continents— Returns all seven continents, each with a list of their countries.continent(code: ID!)— Returns a single continent by code (like"NA"for North America,"EU"for Europe).languages— Returns all languages in the dataset.language(code: ID!)— Returns a single language by code (like"en"for English).
3.3 — The Country Type — A Closer Look
| Field | Type | Description |
|---|---|---|
code |
ID! |
Two-letter ISO country code (e.g., CA) |
name |
String! |
English name (e.g., Canada) |
native |
String! |
Native name (e.g., Canada) |
phone |
String! |
Phone dialing code (e.g., 1) |
capital |
String |
Capital city (nullable for some territories) |
currency |
String |
Currency code (e.g., CAD) |
emoji |
String! |
Country flag emoji |
emojiU |
String! |
Unicode representation of the emoji |
continent |
Continent! |
The continent this country belongs to |
languages |
[Language!]! |
Array of spoken languages |
states |
[State!]! |
Array of states or provinces |
3.4 — Trying It in the Playground
Before writing any Swift code, you should explore the API in the interactive playground. Open your browser and go to https://countries.trevorblades.com. You will see a GraphQL playground interface where you can write and execute queries.
Try pasting this query into the left panel and clicking the Play button:
query {
country(code: "CA") {
name
native
capital
emoji
currency
languages {
name
}
continent {
name
}
}
}
Try It Yourself!
Spend a few minutes in the playground. Try querying for your own country, list all continents, or filter countries by currency. The more comfortable you are writing queries here, the easier the Swift code will be.
Chapter 4: Setting Up Your Xcode Project
4.1 — Prerequisites
Before you begin, make sure you have the following:
- A Mac running macOS 14 (Sonoma) or later
- Xcode 15.4 or later (download free from the Mac App Store)
- Basic familiarity with Swift (variables, functions, structs)
- An internet connection (to download the schema and make API calls)
4.2 — Create a New Project
- Open Xcode and select Create New Project from the welcome screen (or go to File → New → Project).
- Under the iOS tab, select App and click Next.
- Fill in the project details:
- Product Name:
CountryExplorer - Team: Your personal team (or None for now)
- Organization Identifier:
com.yourname(e.g.,com.johndoe) - Interface: SwiftUI
- Language: Swift
- Product Name:
- Click Next, choose a location to save your project, and click Create.
Why SwiftUI?
SwiftUI is Apple's modern declarative UI framework. It pairs beautifully with GraphQL because both are declarative — you describe what you want, not how to get it. SwiftUI also makes it very easy to react to data changes from network calls.
4.3 — Project Structure
Your project should now have a basic structure. We will be adding these files throughout the tutorial:
CountryExplorer/
├── CountryExplorerApp.swift ← App entry point
├── ContentView.swift ← Main view (we will modify this)
├── Network/
│ └── ApolloClient+Shared.swift ← Apollo client setup
├── GraphQL/
│ ├── schema.graphqls ← Downloaded schema
│ ├── AllCountries.graphql ← Our query files
│ ├── CountryDetail.graphql
│ └── ContinentsList.graphql
├── Views/
│ ├── CountryListView.swift
│ ├── CountryDetailView.swift
│ └── ContinentView.swift
└── CountriesAPI/ ← Generated code (by Apollo)
├── Schema/
└── Operations/
Don't create all these folders yet — we will add them step by step as we progress through each chapter.
Chapter 5: Installing Apollo iOS via Swift Package Manager
5.1 — What is Apollo iOS?
Apollo iOS is the industry-leading GraphQL client for Apple platforms. It is an open-source Swift library that handles everything you need to work with GraphQL:
- Sending queries and mutations to a GraphQL server
- Parsing JSON responses into type-safe Swift models (no manual
Codableconformance!) - Caching responses locally so your app feels fast
- Generating Swift code from your GraphQL operations
5.2 — Adding the Package to Your Xcode Project
We will use Swift Package Manager (SPM), which is Apple's built-in dependency manager and the recommended way to install Apollo iOS.
- In Xcode, go to File → Add Package Dependencies...
- In the search bar at the top right, paste this URL:
https://github.com/apollographql/apollo-ios.git
- Xcode will find the
apollo-iospackage. For Dependency Rule, select Up to Next Major Version and ensure the version starts at2.0.0or later. - Click Add Package. Xcode will download and resolve the package.
- On the next screen, you will see a list of library products. Select only Apollo and make sure it is added to your
CountryExplorertarget.
Important Warning!
Do not select the apollo-ios-cli product. That is the command-line tool for code generation, and if you link it to your app target, it will cause build errors. Only select the Apollo library.
- Click Add Package to finish.
5.3 — Verify Installation
Open your ContentView.swift file and add this import at the top:
import Apollo
Build your project (Cmd + B). If it builds successfully with no errors, Apollo is installed correctly.
5.4 — Installing the Codegen CLI
Apollo iOS includes a powerful code generation command-line tool (CLI). This tool reads your GraphQL schema and query files and generates type-safe Swift code. We need to install it into our project.
- In Xcode's Project Navigator (left sidebar), right-click on your project name (the top-level blue icon, not a file).
- In the context menu, look for a plugin called Install CLI (it might appear under a submenu). Click it.
- A dialog will ask you to grant write access to your project directory. Click Allow.
- Wait for Xcode to build the CLI. This may take a minute or two the first time.
After the plugin finishes, a file called apollo-ios-cli will appear in your project's root directory. This is a symbolic link to the compiled CLI binary.
5.5 — Verify the CLI
Open Terminal, navigate to your project directory, and run:
cd /path/to/your/CountryExplorer
./apollo-ios-cli --version
You should see a version number like 2.x.x printed. If you see an error, try cleaning your Xcode Derived Data (Xcode → Settings → Locations → click the arrow next to Derived Data) and re-running the Install CLI plugin.
Chapter 6: Downloading the GraphQL Schema
6.1 — What is a Schema and Why Do We Need It?
A GraphQL schema is like a blueprint of the entire API. It defines every type, every field, every argument, and every relationship available. Think of it as a comprehensive dictionary that describes all the data the server can provide.
Apollo iOS needs a local copy of this schema to do its magic. During code generation, Apollo reads the schema to understand the types available, then looks at your .graphql query files to see what data you are requesting, and finally generates Swift structs that match exactly. Without the schema, Apollo would not know what types or fields are valid.
6.2 — Initialize the Codegen Configuration
Before we can download the schema, we need to create a configuration file that tells the CLI how to work. Open Terminal in your project root directory and run:
./apollo-ios-cli init \
--schema-namespace CountriesAPI \
--module-type embeddedInTarget \
--target-name CountryExplorer
Let's break down each flag:
--schema-namespace CountriesAPI— This is the name of the Swift namespace (enum) that will contain all generated types. You will write things likeCountriesAPI.AllCountriesQueryin your code.--module-type embeddedInTarget— This tells Apollo to generate code that is embedded directly in your app target (as opposed to a separate Swift package). This is the simplest option for single-target projects.--target-name CountryExplorer— The name of the Xcode target where the generated code will live.
This command creates a file called apollo-codegen-config.json in your project root. This is the central configuration file that controls all code generation behavior.
6.3 — Configure Schema Download
Open apollo-codegen-config.json in a text editor. We need to add a schemaDownload section so the CLI knows where to fetch the schema from. Add this to the top level of the JSON (as a sibling of schemaNamespace, input, and output):
"schemaDownload": {
"downloadMethod": {
"introspection": {
"endpointURL": "https://countries.trevorblades.com",
"httpMethod": {
"POST": {}
}
}
},
"outputPath": "./graphql/schema.graphqls"
}
Also update the input section to point to our graphql folder:
"input": {
"operationSearchPaths": ["./graphql/**/*.graphql"],
"schemaSearchPaths": ["./graphql/**/*.graphqls"]
}
6.4 — Download the Schema
Now create the graphql directory and run the download:
mkdir -p graphql
./apollo-ios-cli fetch-schema
If successful, you should see a file at graphql/schema.graphqls. Open it and you will see the entire Countries API schema written in GraphQL Schema Definition Language (SDL). You will see type definitions like:
type Country {
code: ID!
name: String!
native: String!
phone: String!
continent: Continent!
capital: String
currency: String
languages: [Language!]!
emoji: String!
emojiU: String!
states: [State!]!
}
What does the ! mean?
In GraphQL, an exclamation mark after a type means the field is non-null — the server guarantees it will always return a value. String! means a guaranteed string, while String (without !) means the value could be null. Notice that capital is nullable — some territories don't have a capital city.
Chapter 7: Writing Your First GraphQL Query
7.1 — Understanding .graphql Files
Apollo iOS looks for files with the .graphql extension. These files contain your GraphQL operation definitions — the queries, mutations, and fragments your app will use. Each operation you define in a .graphql file gets turned into a Swift class or struct during code generation.
7.2 — Create the AllCountries Query
In the graphql/ folder, create a new file called AllCountries.graphql. Paste in this query:
query AllCountries {
countries {
code
name
emoji
capital
currency
continent {
name
}
}
}
Let's break down every part of this query:
query— This keyword tells GraphQL we are reading data (not writing).AllCountries— This is the operation name. Apollo will generate a Swift type calledAllCountriesQueryfrom this name.countries— This is a root field on theQuerytype. It returns an array ofCountryobjects.code,name,emoji,capital,currency— These are scalar fields we want from each country.continent { name }— This is a nested object. We are saying: for each country, also fetch its continent, but only get the continent's name.
7.3 — Create the CountryDetail Query
Create another file in the graphql/ folder called CountryDetail.graphql:
query CountryDetail($code: ID!) {
country(code: $code) {
code
name
native
capital
emoji
currency
phone
continent {
name
}
languages {
code
name
native
}
states {
name
}
}
}
This query introduces something new — a variable:
$code: ID!— This declares a variable calledcodeof typeID!(a non-nullable identifier). The dollar sign ($) marks it as a variable.country(code: $code)— We pass the variable as an argument to thecountryfield. This lets us query any country by passing different codes.
7.4 — Create the ContinentsList Query
Create one more file: ContinentsList.graphql:
query ContinentsList {
continents {
code
name
countries {
code
name
emoji
}
}
}
This query demonstrates the power of GraphQL — we are fetching all continents and their countries in a single request. With REST, this would typically require eight separate API calls (one for continents, then one for each continent's countries).
Chapter 8: Running Code Generation
8.1 — What Code Generation Does
Code generation is the step where Apollo reads your schema (.graphqls file) and your query files (.graphql files) and produces Swift source code (.graphql.swift files). This generated code includes:
- Swift structs for every query/mutation you defined
- Nested data models matching the exact shape of each query's response
- Type-safe enums for GraphQL enum types
- Input object types for query arguments
- Schema metadata used internally by the Apollo cache
The beauty of this approach is that you never have to manually define Codable structs for your network responses. If the schema says a field is a String!, the generated Swift property will be a String (not optional). If the schema says a field is String (nullable), it will be String? in Swift.
8.2 — Run the Generator
Open Terminal in your project root directory and run:
./apollo-ios-cli generate
If everything is configured correctly, you will see output indicating which files were generated. A new directory called CountriesAPI/ will appear in your project root with two subdirectories:
Sources/Schema/— Contains generated types for your schema (enums, input types, schema metadata).Sources/Operations/Queries/— Contains generated Swift code for each of your.graphqlquery files.
8.3 — Add Generated Files to Xcode
If you are using an Xcode project (not a Package.swift), you need to manually add the generated files:
- In Xcode, right-click on your
CountryExplorerfolder in the Project Navigator. - Select Add Files to "CountryExplorer"...
- Navigate to the
CountriesAPI/folder in your project root. - Select the entire
CountriesAPIfolder and click Add. Make sure Copy items if needed is unchecked (we want references, not copies) and theCountryExplorertarget is checked.
8.4 — Explore the Generated Code
Open AllCountriesQuery.graphql.swift. You will see something like this (simplified):
// @generated
// This file was automatically generated and should not be edited.
class AllCountriesQuery: GraphQLQuery {
static let operationName: String = "AllCountries"
static let operationDocument: DocumentType = .notPersisted(
definition: .init("query AllCountries { ... }")
)
struct Data: SelectionSet {
let __data: DataDict
var countries: [Country] { __data["countries"] }
struct Country: SelectionSet {
let __data: DataDict
var code: String { __data["code"] }
var name: String { __data["name"] }
var emoji: String { __data["emoji"] }
var capital: String? { __data["capital"] }
var currency: String? { __data["currency"] }
var continent: Continent { __data["continent"] }
struct Continent: SelectionSet {
var name: String { __data["name"] }
}
}
}
}
Never Edit Generated Files!
Files with the .graphql.swift extension are auto-generated. Any changes you make will be overwritten the next time you run code generation. If you want to change the shape of data, modify your .graphql query files and re-run the generator.
Chapter 9: Building the Network Layer
9.1 — What is ApolloClient?
The ApolloClient is the central object in Apollo iOS. It is responsible for sending your GraphQL operations to the server, receiving the response, parsing it into your generated models, and caching the results. You typically create one shared instance and use it throughout your app.
9.2 — Create the Shared Client
In your Xcode project, create a new Swift file called Network.swift (inside the CountryExplorer folder). Add this code:
import Foundation
import Apollo
class Network {
static let shared = Network()
private(set) lazy var apollo: ApolloClient = {
let url = URL(string: "https://countries.trevorblades.com")!
return ApolloClient(url: url)
}()
}
Let's examine each piece:
static let shared = Network()— This creates a singleton. A singleton ensures there is only one instance ofNetworkin the entire app, so every view shares the same Apollo client and cache.private(set) lazy var apollo— Thelazykeyword means theApolloClientis only created when it is first accessed, not at app launch.private(set)means other code can read theapolloproperty but cannot replace it.ApolloClient(url: url)— This creates anApolloClientwith default settings: it usesURLSessionfor networking, HTTPPOSTfor requests, and an in-memory normalized cache for caching responses.
9.3 — Understanding the Defaults
When you create an ApolloClient with just a URL, Apollo sets up sensible defaults behind the scenes:
- Transport: Uses
RequestChainNetworkTransportwithURLSession. Every query is sent as an HTTP POST with the GraphQL operation in the request body. - Cache: Uses
InMemoryNormalizedCache. This stores response data in memory, organized by object identity. If two different queries fetch the same country, the cache stores it only once. - Request format: Sends JSON with keys
"query"(the GraphQL string),"variables"(any arguments), and"operationName".
9.4 — How a Request Flows
When you call apollo.fetch(query:), here is what happens step by step:
- Apollo serializes your query into a JSON request body.
- The request is sent via HTTP POST to the endpoint URL.
- The server processes the query and returns a JSON response.
- Apollo parses the JSON into your generated Swift data model.
- The parsed data is stored in the normalized cache.
- Your completion handler (or async result) receives the type-safe data.
Chapter 10: Fetching Data & Displaying in SwiftUI
10.1 — Create a ViewModel
Create a new Swift file called CountryListViewModel.swift:
import Foundation
import Apollo
@MainActor
class CountryListViewModel: ObservableObject {
@Published var countries: [AllCountriesQuery.Data.Country] = []
@Published var isLoading = false
@Published var error: String?
func fetchCountries() {
isLoading = true
error = nil
Network.shared.apollo.fetch(
query: AllCountriesQuery(),
cachePolicy: .fetchIgnoringCacheData
) { [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .success(let graphQLResult):
if let countries = graphQLResult.data?.countries {
self?.countries = countries
}
if let errors = graphQLResult.errors {
self?.error = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
}
case .failure(let networkError):
self?.error = networkError.localizedDescription
}
}
}
}
}
Key concepts in this code:
@MainActor— Ensures all property changes happen on the main thread (required for UI updates).@Published— SwiftUI will automatically re-render views when these properties change.AllCountriesQuery()— This is the generated Swift class from ourAllCountries.graphqlfile.[weak self]— Prevents memory leaks by avoiding a strong reference cycle in the closure.graphQLResult.data?.countries— The response data, already parsed into type-safe Swift structs.graphQLResult.errors— GraphQL responses can include both data and errors simultaneously.
10.2 — Build the Country List View
Create CountryListView.swift:
import SwiftUI
struct CountryListView: View {
@StateObject private var viewModel = CountryListViewModel()
@State private var searchText = ""
var filteredCountries: [AllCountriesQuery.Data.Country] {
if searchText.isEmpty {
return viewModel.countries
}
return viewModel.countries.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Loading countries...")
} else if let error = viewModel.error {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundStyle(.red)
Text(error)
.multilineTextAlignment(.center)
Button("Retry") {
viewModel.fetchCountries()
}
}
.padding()
} else {
List(filteredCountries, id: \.code) { country in
NavigationLink {
CountryDetailView(countryCode: country.code)
} label: {
HStack(spacing: 12) {
Text(country.emoji)
.font(.largeTitle)
VStack(alignment: .leading) {
Text(country.name)
.font(.headline)
Text(country.continent.name)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if let currency = country.currency {
Text(currency)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.blue.opacity(0.1))
.clipShape(.capsule)
}
}
}
}
.searchable(text: $searchText, prompt: "Search")
}
}
.navigationTitle("Countries")
.task { viewModel.fetchCountries() }
}
}
}
10.3 — Build the Country Detail View
Create CountryDetailView.swift:
import SwiftUI
struct CountryDetailView: View {
let countryCode: String
@State private var country: CountryDetailQuery.Data.Country?
@State private var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let country {
List {
Section {
Text(country.emoji)
.font(.system(size: 80))
.frame(maxWidth: .infinity)
}
Section("General") {
row("Native Name", country.native)
row("Capital", country.capital ?? "N/A")
row("Continent", country.continent.name)
row("Phone Code", "+\(country.phone)")
row("Currency", country.currency ?? "N/A")
}
Section("Languages") {
ForEach(country.languages, id: \.code) { lang in
HStack {
Text(lang.name)
Spacer()
Text(lang.native)
.foregroundStyle(.secondary)
}
}
}
if !country.states.isEmpty {
Section("States / Provinces (\(country.states.count))") {
ForEach(country.states, id: \.name) {
Text($0.name)
}
}
}
}
}
}
.navigationTitle(country?.name ?? "Loading...")
.task { await loadCountry() }
}
private func row(_ title: String, _ value: String) -> some View {
HStack {
Text(title).foregroundStyle(.secondary)
Spacer()
Text(value)
}
}
private func loadCountry() async {
Network.shared.apollo.fetch(
query: CountryDetailQuery(code: countryCode)
) { result in
DispatchQueue.main.async {
isLoading = false
if case .success(let res) = result {
country = res.data?.country
}
}
}
}
}
10.4 — Wire Up the App
Update your ContentView.swift (or your app's main entry point) to show the CountryListView:
import SwiftUI
struct ContentView: View {
var body: some View {
CountryListView()
}
}
Build and run your app (Cmd + R). You should see a list of all countries with their flags, names, continents, and currencies. Tapping a country navigates to a detail screen showing languages, states, and more — all powered by GraphQL.
Chapter 11: Using Variables and Arguments
11.1 — What Are Variables?
In GraphQL, variables let you parameterize your queries. Instead of hardcoding values into the query string, you define named placeholders that are filled in at runtime. This is critical for building dynamic apps where the user's actions determine what data to fetch.
We already used a variable in our CountryDetail query:
query CountryDetail($code: ID!) {
country(code: $code) {
name
...
}
}
In Swift, Apollo makes this incredibly clean. The generated CountryDetailQuery class has an initializer that requires the code parameter:
// This is how you use it in Swift:
let query = CountryDetailQuery(code: "CA")
// Or with a variable:
let userSelectedCode = "JP"
let query = CountryDetailQuery(code: userSelectedCode)
11.2 — The Filter Argument
The Countries API supports an optional filter argument on its root query fields. Let's create a query that filters countries by continent. Create a new file called FilteredCountries.graphql:
query FilteredCountries($filter: CountryFilterInput) {
countries(filter: $filter) {
code
name
emoji
capital
}
}
After running code generation again, you can use this in Swift:
// Fetch only European countries
let filter = GraphQLNullable<CountriesAPI.CountryFilterInput>(
.init(continent: .init(.init(eq: .some("EU"))))
)
let query = FilteredCountriesQuery(filter: filter)
11.3 — Variable Types in GraphQL
| GraphQL Type | Swift Equivalent | Example |
|---|---|---|
ID! |
String |
"CA" |
String! |
String |
"Canada" |
String |
GraphQLNullable<String> |
.some("Canada") or .null |
Int! |
Int |
42 |
Float |
GraphQLNullable<Double> |
.some(3.14) |
Boolean! |
Bool |
true |
[String!]! |
[String] |
["en", "fr"] |
CustomInput! |
CustomInput |
Generated struct |
Chapter 12: Using Fragments for Reusable Fields
12.1 — What Is a Fragment?
A fragment is a reusable set of fields that you can include in multiple queries. Think of it like a Swift protocol or a reusable component — you define the fields once and reference them wherever needed.
This is extremely useful when multiple screens in your app need the same subset of data about a type. Instead of duplicating field selections across queries, you write a fragment once.
12.2 — Create a Fragment
Create a new file called CountryBasicInfo.graphql in your graphql/ folder:
fragment CountryBasicInfo on Country {
code
name
emoji
capital
currency
continent {
name
}
}
Now you can use this fragment in any query with the spread operator (...):
query AllCountriesWithFragment {
countries {
...CountryBasicInfo
languages {
name
}
}
}
12.3 — Using Fragments in Swift
After code generation, Apollo creates a separate Swift struct for the fragment. Any query that uses the fragment will have a property called fragments that gives you access to the fragment's data:
// Access fragment data
let country = result.data?.countries.first
let basicInfo = country?.fragments.countryBasicInfo
print(basicInfo?.name) // "Canada"
print(basicInfo?.emoji) // "🇨🇦"
print(basicInfo?.capital) // "Ottawa"
Chapter 13: Error Handling
13.1 — Types of Errors
When working with GraphQL in iOS, you will encounter three distinct types of errors:
1. Network Errors
These happen when the HTTP request itself fails — no internet connection, server is down, timeout, DNS failure, etc. In Apollo iOS, these appear as a .failure case in the Result:
case .failure(let error):
// The request never reached the server,
// or the response was not valid HTTP
print("Network error: \(error.localizedDescription)")
2. GraphQL Errors
These happen when the server successfully received your request but something went wrong with the query itself — invalid field, authorization failure, or a resolver error. These come in the .success case alongside potentially partial data:
case .success(let result):
if let errors = result.errors {
// The server returned errors, but may also
// have returned partial data
for error in errors {
print("GraphQL error: \(error.message ?? "")")
}
}
if let data = result.data {
// Process whatever data was returned
}
3. Null Data
Sometimes a query succeeds with no errors, but the data is nil. This can happen if you query for a specific item that does not exist (like a country code that is not in the database):
let result = try await apolloClient.fetch(
query: CountryDetailQuery(code: "XX") // Invalid code
)
// result.data?.country will be nil
13.2 — A Robust Error Handling Pattern
func fetchData<Query: GraphQLQuery>(
query: Query
) async throws -> Query.Data {
return try await withCheckedThrowingContinuation { cont in
Network.shared.apollo.fetch(query: query) { result in
switch result {
case .success(let graphQLResult):
if let data = graphQLResult.data {
cont.resume(returning: data)
} else if let errors = graphQLResult.errors {
let msg = errors.map { $0.localizedDescription }
.joined(separator: ", ")
cont.resume(throwing: AppError.graphQL(msg))
} else {
cont.resume(throwing: AppError.noData)
}
case .failure(let error):
cont.resume(throwing: AppError.network(error))
}
}
}
}
enum AppError: LocalizedError {
case network(Error)
case graphQL(String)
case noData
var errorDescription: String? {
switch self {
case .network(let err): return "Network: \(err.localizedDescription)"
case .graphQL(let msg): return "Server: \(msg)"
case .noData: return "No data returned"
}
}
}
Chapter 14: Understanding the Apollo Cache
14.1 — How the Normalized Cache Works
Apollo iOS includes a powerful normalized cache. When you fetch data, Apollo does not just store the raw JSON response. Instead, it breaks the response into individual objects and stores them by identity (usually the id or unique key of each object). This means:
- If two different queries return the same country, it is stored only once in memory.
- When data is updated, every query that references that object sees the update.
- Subsequent requests for the same data can be served from the cache instantly.
14.2 — Cache Policies
When you call apollo.fetch(), you can specify a cache policy that controls where data comes from:
| Policy | Behavior | Best For |
|---|---|---|
.returnCacheDataElseFetch |
Returns cached data if available, fetches from network only if cache misses | Data that rarely changes (country info) |
.fetchIgnoringCacheData |
Always fetches from the network, then updates the cache | Data that changes often (feeds, scores) |
.returnCacheDataDontFetch |
Only returns cached data, never makes a network request | Offline-first features |
.returnCacheDataAndFetch |
Immediately returns cached data, then fetches fresh data in the background | Best of both worlds — fast UI + fresh data |
Here is how to use a cache policy:
Network.shared.apollo.fetch(
query: AllCountriesQuery(),
cachePolicy: .returnCacheDataElseFetch
) { result in
// Handle result
}
14.3 — Watching Queries
Apollo iOS can also watch a query — your closure is called once immediately with current data (from cache or network), and then again whenever the cached data for that query changes:
let watcher = Network.shared.apollo.watch(
query: AllCountriesQuery()
) { result in
// Called immediately, and again whenever
// cached country data changes
}
// Don't forget to cancel when done:
watcher.cancel()
Chapter 15: Building the Complete Country Explorer App
Let's put everything together into a polished, complete app with a tab-based navigation structure.
15.1 — Update the App Entry Point
import SwiftUI
@main
struct CountryExplorerApp: App {
var body: some Scene {
WindowGroup {
TabView {
CountryListView()
.tabItem {
Label("Countries", systemImage: "globe")
}
ContinentsView()
.tabItem {
Label("Continents", systemImage: "map")
}
}
}
}
}
15.2 — Create the Continents View
import SwiftUI
struct ContinentsView: View {
@State private var continents: [ContinentsListQuery.Data.Continent] = []
@State private var isLoading = true
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView()
} else {
List(continents, id: \.code) { continent in
Section(continent.name) {
ForEach(continent.countries, id: \.code) { country in
NavigationLink {
CountryDetailView(countryCode: country.code)
} label: {
HStack {
Text(country.emoji)
Text(country.name)
}
}
}
}
}
}
}
.navigationTitle("Continents")
.task { await loadContinents() }
}
}
private func loadContinents() async {
Network.shared.apollo.fetch(
query: ContinentsListQuery()
) { result in
DispatchQueue.main.async {
isLoading = false
if case .success(let res) = result {
continents = res.data?.continents ?? []
}
}
}
}
}
15.3 — Complete Checklist
Before you run the app, verify you have:
- Added Apollo iOS via Swift Package Manager (Chapter 5)
- Downloaded the schema to
graphql/schema.graphqls(Chapter 6) - Created all
.graphqlquery files in thegraphql/folder (Chapter 7) - Run
./apollo-ios-cli generateand added the generated files to Xcode (Chapter 8) - Created
Network.swiftwith the sharedApolloClient(Chapter 9) - Created all SwiftUI views (Chapters 10 and 15)
Press Cmd + R to build and run. You should see a beautiful tabbed app where you can browse all countries, search them, view detailed info with languages and states, and explore by continent.
Chapter 16: Where to Go From Here
16.1 — What You Learned
Congratulations! You have gone from zero GraphQL knowledge to building a working iOS app.
Here is a summary of everything you covered:
- What GraphQL is and how it differs from REST
- GraphQL queries, variables, arguments, and fragments
- Setting up an Xcode project with Apollo iOS 2.x via SPM
- Downloading a schema with the Codegen CLI
- Writing
.graphqlquery files and running code generation - Creating an
ApolloClientand fetching type-safe data - Displaying GraphQL data in SwiftUI views with proper error handling
- Understanding Apollo's normalized cache and cache policies
- Building a multi-screen app with navigation and search
16.2 — Recommended Next Steps
- Mutations: Try connecting to a GraphQL API that supports mutations (like a to-do list API). Practice creating, updating, and deleting data.
- Subscriptions: Learn how to set up WebSocket subscriptions for real-time features like live chat or notifications.
- Modularization: For larger projects, explore generating Apollo code into a separate Swift Package instead of embedding in your target.
- SQLite Cache: Replace the in-memory cache with the persistent SQLite cache (
ApolloSQLite) so data survives app relaunches. - Custom Interceptors: Learn to customize the request chain for adding authentication headers, logging, or retry logic.
- Persisted Queries: For production apps, persisted queries reduce bandwidth by sending a hash instead of the full query string.
16.3 — Resources
Here are the best places to continue learning:
- Apollo iOS Official Docs:
apollographql.com/docs/ios - Apollo iOS GitHub:
github.com/apollographql/apollo-ios - Apollo iOS Tutorial (Odyssey):
apollographql.com/tutorials/apollo-ios-swift-part1 - GraphQL Specification:
spec.graphql.org - Countries API Playground:
countries.trevorblades.com
End of Tutorial
Happy coding! 🚀