All Articles
CategoryiOS
Reading Time
19 min read
Published
2024-01-05
Word Count
1,536words

Grab a coffee — this one is a deep dive!

GraphQL in iOS: Apollo Client and Code Generation

Summary

GraphQL vs REST, Apollo iOS client setup, code generation, caching strategies, pagination, optimistic UI, and subscriptions.

  • Apollo iOS 1.0+ provides full type safety through Swift code generation
  • The apollo-ios-cli generate command automatically produces Swift types on schema changes
  • The normalized cache stores every object under a TypeName:id key
  • GraphQL was invented at Facebook in 2012 for the News Feed and open-sourced in 2015
GraphQL in iOS: Apollo Client and Code Generation

When working with REST APIs, you run into over-fetching (pulling too much data) and under-fetching (missing data, N+1 requests). GraphQL lets the client request exactly the data it needs. With the Apollo iOS client, you can use type-safe GraphQL in Swift.

💡 Quick Note: Apollo iOS 1.0+ provides full type safety through Swift code generation. This guide covers the Apollo iOS 1.x APIs.

Table of Contents


GraphQL vs REST

Feature
REST
GraphQL
Data fetching
Fixed endpoints
Flexible queries
Over-fetching
✅ Common
❌ None
Under-fetching
✅ N+1 requests
❌ Single query
Versioning
/v1, /v2
Schema evolution
Caching
HTTP cache (basic)
Normalized cache
Type safety
Manual
Automatic (codegen)
Tooling
Swagger/OpenAPI
GraphiQL, Apollo Studio

Apollo iOS Setup

swift
1// Package.swift
2dependencies: [
3 .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0"),
4]
5// Target dependency: "Apollo", "ApolloWebSocket"
swift
1// Apollo client setup
2import Apollo
3 
4class Network {
5 static let shared = Network()
6 let apollo: ApolloClient
7 
8 init() {
9 let url = URL(string: "https://api.example.com/graphql")!
10 let store = ApolloStore(cache: InMemoryNormalizedCache())
11 let provider = DefaultInterceptorProvider(store: store)
12 let transport = RequestChainNetworkTransport(
13 interceptorProvider: provider,
14 endpointURL: url
15 )
16 apollo = ApolloClient(networkTransport: transport, store: store)
17 }
18}

Schema and Code Generation

graphql
1# Queries/GetProducts.graphql
2query GetProducts($first: Int!, $after: String) {
3 products(first: $first, after: $after) {
4 edges {
5 node {
6 id
7 name
8 price
9 category {
10 id
11 name
12 }
13 reviews {
14 rating
15 text
16 }
17 }
18 }
19 pageInfo {
20 hasNextPage
21 endCursor
22 }
23 }
24}
25 
26# Mutations/CreateOrder.graphql
27mutation CreateOrder($input: CreateOrderInput!) {
28 createOrder(input: $input) {
29 id
30 status
31 total
32 items {
33 product { name }
34 quantity
35 }
36 }
37}

Code generation automatically produces Swift types. It's run with the apollo-ios-cli generate command.

Query and Mutation

swift
1// Query
2func fetchProducts() async throws -> [GetProductsQuery.Data.Products.Edge.Node] {
3 return try await withCheckedThrowingContinuation { continuation in
4 Network.shared.apollo.fetch(
5 query: GetProductsQuery(first: 20, after: nil),
6 cachePolicy: .fetchIgnoringCacheCompletely
7 ) { result in
8 switch result {
9 case .success(let response):
10 let products = response.data?.products.edges.map { $0.node } ?? []
11 continuation.resume(returning: products)
12 case .failure(let error):
13 continuation.resume(throwing: error)
14 }
15 }
16 }
17}
18 
19// Mutation
20func createOrder(items: [OrderItemInput]) async throws -> CreateOrderMutation.Data.CreateOrder {
21 let input = CreateOrderInput(items: items)
22 return try await withCheckedThrowingContinuation { continuation in
23 Network.shared.apollo.perform(mutation: CreateOrderMutation(input: input)) { result in
24 switch result {
25 case .success(let response):
26 if let order = response.data?.createOrder {
27 continuation.resume(returning: order)
28 }
29 case .failure(let error):
30 continuation.resume(throwing: error)
31 }
32 }
33 }
34}

Caching Strategies

Apollo iOS uses a normalized cache — every object is stored by its id and updated automatically:

swift
1// Cache policy options
2.returnCacheDataElseFetch // Cache first, network if missing
3.fetchIgnoringCacheData // Always network
4.returnCacheDataDontFetch // Cache only
5.returnCacheDataAndFetch // Show cache, update in background

Pagination

Cursor-based pagination for infinite scroll:

swift
1class ProductListViewModel: ObservableObject {
2 @Published var products: [ProductNode] = []
3 private var endCursor: String?
4 private var hasNextPage = true
5 
6 func loadMore() async {
7 guard hasNextPage else { return }
8 let result = try? await fetchProducts(after: endCursor)
9 products.append(contentsOf: result?.nodes ?? [])
10 endCursor = result?.pageInfo.endCursor
11 hasNextPage = result?.pageInfo.hasNextPage ?? false
12 }
13}

Optimistic UI

swift
1// Update the UI without waiting for the mutation result
2Network.shared.apollo.perform(
3 mutation: ToggleFavoriteMutation(productId: id),
4 optimisticUpdate: { cache in
5 // Update the cache immediately
6 try? cache.updateObject(ofType: ProductNode.self, withKey: "Product:\(id)") { product in
7 product.isFavorite = !product.isFavorite
8 }
9 }
10)

Subscriptions (Real-time)

swift
1// Real-time updates via WebSocket
2let subscription = Network.shared.apollo.subscribe(
3 subscription: OrderStatusSubscription(orderId: orderId)
4) { result in
5 switch result {
6 case .success(let response):
7 if let status = response.data?.orderStatusChanged.status {
8 self.orderStatus = status
9 }
10 case .failure(let error):
11 print("Subscription error: \(error)")
12 }
13}

Error Handling

GraphQL partial errors — some fields can succeed while others fail:

swift
1func handleResult(_ result: GraphQLResult<GetProductsQuery.Data>) {
2 if let errors = result.errors {
3 for error in errors {
4 print("GraphQL Error: \(error.message ?? "Unknown")")
5 // The path shows which field failed
6 print("Path: \(error.path ?? [])")
7 }
8 }
9 if let data = result.data {
10 // Partial data is usable
11 self.products = data.products.edges.map { $0.node }
12 }
13}

Using Fragments

Fragments let you share repeated groups of fields — they apply the DRY principle in GraphQL:

graphql
1# Fragments/ProductFields.graphql
2fragment ProductFields on Product {
3 id
4 name
5 price
6 imageUrl
7 category {
8 id
9 name
10 }
11}
12 
13# Fragments/UserFields.graphql
14fragment UserFields on User {
15 id
16 name
17 email
18 avatar
19}
20 
21# Queries/GetProductDetail.graphql
22query GetProductDetail($id: ID!) {
23 product(id: $id) {
24 ...ProductFields
25 description
26 stock
27 reviews {
28 id
29 rating
30 text
31 author {
32 ...UserFields
33 }
34 }
35 }
36}
37 
38# Queries/GetCart.graphql
39query GetCart {
40 cart {
41 items {
42 quantity
43 product {
44 ...ProductFields
45 }
46 }
47 totalPrice
48 }
49}

Benefits of using fragments:

Benefit
Description
Prevents code duplication
You don't rewrite the same fields in every query
Type consistency
Code generation turns fragments into separate Swift structs
Easier maintenance
Adding/removing fields happens in one place
Cache compatibility
Apollo's normalized cache automatically maps fragments

Auth Token with a Custom Interceptor

You can add an auth token to every request automatically using the Apollo iOS interceptor chain:

swift
1// AuthInterceptor.swift
2class AuthInterceptor: ApolloInterceptor {
3 let id = "AuthInterceptor"
4 private let tokenProvider: TokenProvider
5 
6 init(tokenProvider: TokenProvider) {
7 self.tokenProvider = tokenProvider
8 }
9 
10 func interceptAsync<Operation: GraphQLOperation>(
11 chain: RequestChain,
12 request: HTTPRequest<Operation>,
13 response: HTTPResponse<Operation>?,
14 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
15 ) {
16 Task {
17 if let token = try? await tokenProvider.getValidToken() {
18 request.addHeader(name: "Authorization", value: "Bearer \(token)")
19 }
20 chain.proceedAsync(request: request, response: response, interceptor: self, completion: completion)
21 }
22 }
23}
24 
25// LoggingInterceptor - for debugging
26class LoggingInterceptor: ApolloInterceptor {
27 let id = "LoggingInterceptor"
28 
29 func interceptAsync<Operation: GraphQLOperation>(
30 chain: RequestChain,
31 request: HTTPRequest<Operation>,
32 response: HTTPResponse<Operation>?,
33 completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
34 ) {
35 let operationName = Operation.operationName
36 let startTime = CFAbsoluteTimeGetCurrent()
37 print("[GraphQL] --> \(operationName)")
38 
39 chain.proceedAsync(request: request, response: response, interceptor: self) { result in
40 let duration = CFAbsoluteTimeGetCurrent() - startTime
41 switch result {
42 case .success:
43 print("[GraphQL] <-- \(operationName) (\(String(format: "%.2f", duration * 1000))ms)")
44 case .failure(let error):
45 print("[GraphQL] <-- \(operationName) FAIL: \(error)")
46 }
47 completion(result)
48 }
49 }
50}
51 
52// Custom InterceptorProvider
53class NetworkInterceptorProvider: DefaultInterceptorProvider {
54 let tokenProvider: TokenProvider
55 
56 init(store: ApolloStore, tokenProvider: TokenProvider) {
57 self.tokenProvider = tokenProvider
58 super.init(store: store)
59 }
60 
61 override func interceptors<Operation: GraphQLOperation>(
62 for operation: Operation
63 ) -> [ApolloInterceptor] {
64 var interceptors = super.interceptors(for: operation)
65 interceptors.insert(AuthInterceptor(tokenProvider: tokenProvider), at: 0)
66 interceptors.insert(LoggingInterceptor(), at: 0)
67 return interceptors
68 }
69}

Best Practices

  1. Use fragments — share repeated fields
  2. Persisted queries — don't send the query string in production, send a hash
  3. Batch queries — send multiple queries in a single request
  4. Code generation in CI — regenerate automatically on every schema change
  5. Error boundary — partial data handling
  6. Interceptor chain — write auth, logging, and retry logic as interceptors
  7. Fragment colocation — place fragments next to the view that uses them
  8. Query complexity — avoid deeply nested queries, set a server-side depth limit

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Reader Reward

Congratulations! Since you read this article all the way to the end, I have a special gift for you:

Sources:

Tags

#GraphQL#Apollo#iOS#API#networking#code generation#Swift
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

We respect your privacy. You can unsubscribe at any time.

Share