All skills
apollographql avatar

/apollo-ios

@e50044c official
by Apollo GraphQLapollographql/skills115 stars
13

Guide for building Apple-platform applications with Apollo iOS, the strongly-typed GraphQL client for Swift. Use this skill when: (1) adding Apollo iOS to a Swift Package Manager or Xcode project, (2) configuring `apollo-codegen-config.json` and running code generation, (3) configuring an `ApolloClient` with auth, interceptors, and caching, (4) writing queries, mutations, or subscriptions from SwiftUI views, (5) writing tests against generated operation mocks.

Use this Skill: https://skilld.dev/gh/apollographql/skills/apollo-ios

This session only. Nothing lands on disk.

referencescaching.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Caching

Apollo iOS ships a normalized cache: records are keyed by object identity so multiple queries that reference the same entity share storage. When a mutation or new fetch updates a record, every watcher that depends on it is notified automatically.

This reference covers store selection, cache keys (declarative @typePolicy directives and programmatic fallback), watching, manual reads/writes, and clearing.

Choose a store

The ApolloStore is backed by a NormalizedCache. Two implementations ship with the SDK:

In-memory cache (default)

import Apollo

let store = ApolloStore()
// Equivalent to:
// let store = ApolloStore(cache: InMemoryNormalizedCache())

Lost on app termination. Good for data that doesn't need to persist (search results, transient UI).

SQLite cache (persistent)

import Apollo
import ApolloSQLite

let cacheURL = try FileManager.default
  .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
  .appendingPathComponent("apollo_cache.sqlite")

let cache = try SQLiteNormalizedCache(fileURL: cacheURL)
let store = ApolloStore(cache: cache)

Persists across launches. Prefer .cachesDirectory (not .documentDirectory) so the OS can evict the file under storage pressure.

Pass the store to ApolloClient and to RequestChainNetworkTransport / WebSocketTransport when you build a custom transport (see setup.md).

Cache keys — prefer @typePolicy

For the cache to deduplicate records across queries, the SDK has to know which field identifies each object. Declare this declaratively with the @typePolicy schema directive.

Declare cache keys in a schema extension file

Create a new .graphqls file (for example cacheKeys.graphqls) and include it in input.schemaSearchPaths in apollo-codegen-config.json.

# cacheKeys.graphqls
extend type User @typePolicy(keyFields: "id")
extend type Book @typePolicy(keyFields: "isbn")
extend type Author @typePolicy(keyFields: "firstName lastName")
  • Single field: keyFields: "id"
  • Composite key: space-separate the fields (keyFields: "firstName lastName")
  • One @typePolicy per type you want deduplicated.

Regenerate after editing:

./apollo-ios-cli generate

See the official Cache Key Resolution page for the full directive reference.

@fieldPolicy — cache resolution for fields with arguments

@typePolicy tells the cache how to identify an object. @fieldPolicy tells the cache how to resolve a field call with arguments to a cache record. Use them together.

The motivating problem: with only @typePolicy(keyFields: "id") on User, calling client.fetch(query: GetUserQuery(id: "42")) always hits the network even when a previous query already populated User:42 in the cache. The cache stores the user record, but it has no way to know that the operation user(id: "42") should resolve to it. @fieldPolicy closes that gap.

Declare it on the type that owns the field (typically Query) in the same schema extension file used for @typePolicy:

# cacheKeys.graphqls
extend type User @typePolicy(keyFields: "id")

extend type Query
  @fieldPolicy(forField: "user", keyArgs: "id")
  @fieldPolicy(forField: "book", keyArgs: "isbn")

Now client.fetch(query: GetUserQuery(id: "42"), cachePolicy: .cacheFirst) returns the cached User:42 record without a network round-trip. The same applies to cacheOnly reads — they succeed against fields the app has never directly fetched, as long as the underlying object exists in the cache.

keyArgs is space-delimited and order-significant — argument order in the string determines the structure of the generated cache key:

# Multi-argument field — resolves to one cache record per (species, habitat) pair.
extend type Query @fieldPolicy(forField: "allAnimals", keyArgs: "species habitat")

A field can have multiple @fieldPolicy directives stacked when the same query type owns multiple resolvable fields. Add them as you ship features that benefit from cache deduplication — there is no cost to declaring more.

When to use which:

Goal Directive
Identify a cached object by its own fields @typePolicy(keyFields: ...) on the object type
Resolve a query field's arguments to that cached object @fieldPolicy(forField: ..., keyArgs: ...) on the parent type
Cache-key a parameterized collection (e.g., allAnimals(species:, habitat:)) @fieldPolicy(forField: ..., keyArgs: ...) on Query

Programmatic cache keys (advanced fallback)

Use programmatic keys only when @typePolicy cannot express what you need — for example, keys derived from nested fields, or interface types that key differently per concrete type.

Apollo iOS generates a SchemaConfiguration.swift stub inside your schema module. Edit the cacheKeyInfo(for:object:) method:

import ApolloAPI

public enum SchemaConfiguration: SchemaConfiguration_Compat {
  public static func cacheKeyInfo(
    for type: Object,
    object: ObjectData
  ) -> CacheKeyInfo? {
    switch type {
    case Objects.User:
      guard let id = object["id"] as? String else { return nil }
      return CacheKeyInfo(id: id)

    case Objects.Comment:
      // Composite key derived from author + timestamp.
      guard let authorID = (object["author"] as? ObjectData)?["id"] as? String,
            let createdAt = object["createdAt"] as? String else {
        return nil
      }
      return CacheKeyInfo(id: "\(authorID)_\(createdAt)")

    default:
      return nil
    }
  }
}

See the Programmatic Cache Keys documentation for the full API.

Watching the cache

client.watch(query:resultHandler:) fires every time records relevant to the query change — whether from a fetch, a mutation response, or a manual cache write. Use watchers as the reactive primitive for SwiftUI views. See operations.md for the canonical @Observable view-model pattern.

When a mutation returns a response whose selection set matches existing cached records (same types, same cache keys, same requested fields), the cache is updated automatically. For anything else — inserting into a list, removing an item, optimistic UI — update the cache manually.

Manual cache reads/writes

The ApolloStore exposes transactional read/write access. Always run writes inside withinReadWriteTransaction to avoid partial updates.

Read

let data = try await apolloClient.store.withinReadTransaction { tx in
  try await tx.read(query: GetUserQuery(id: id))
}

Write after a successful mutation (optimistic UI + confirmation)

func addTodo(_ title: String) async throws {
  // 1. Optimistic local write — UI updates immediately via watchers.
  try await apolloClient.store.withinReadWriteTransaction { tx in
    try await tx.update(query: GetTodosQuery()) { data in
      data.todos.append(
        GetTodosQuery.Data.Todo(
          _dataDict: .init(
            data: ["__typename": "Todo", "id": "temp", "title": title, "completed": false],
            fulfilledFragments: []
          )
        )
      )
    }
  }

  // 2. Perform the mutation; its response will update the cache with the real record.
  _ = try await apolloClient.perform(mutation: AddTodoMutation(title: title))
}

Exact method names on the transaction:

  • read(query:) — read a full query's data from the cache.
  • update(query:) / updateObject(ofType:withKey:) — mutate a cache entry and publish.
  • write(data:for:) / write(selectionSet:withKey:) — overwrite a cache entry.

See ApolloStore.ReadTransaction and ApolloStore.ReadWriteTransaction in the SDK source for the full signatures.

Clear the cache

On logout, or any time you need a clean slate:

try await apolloClient.clearCache()

This clears the entire normalized cache. For finer-grained clears (single record, single type), use withinReadWriteTransaction and remove or overwrite the relevant records.

Ground rules

  • Declare @typePolicy for every type you want deduplicated in the cache. This is the recommended default.
  • Pair @typePolicy with @fieldPolicy on argument-bearing query fields (Query.user(id:), Query.book(isbn:), etc.) so cache reads can resolve to the deduplicated record without going through the network.
  • Only drop to programmatic cacheKeyInfo when neither @typePolicy nor @fieldPolicy can express the key.
  • Always wrap cache writes in withinReadWriteTransaction — concurrent writes without a transaction corrupt state.
  • Never access the raw cache (InMemoryNormalizedCache / SQLiteNormalizedCache) directly. Always go through ApolloStore.
  • Clear the cache on logout so the next user doesn't see cached data from the previous session.
  • When adding a new type to your schema, add a @typePolicy entry in the same PR. Adding it later is trivial; noticing the deduplication bug in production is painful.

Source: SKILL.md on GitHub

1 warning16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides guidance for implementing Apollo iOS and correctly mitigates potential indirect prompt injection risks associated with fetching external GraphQL schemas and metadata. It utilizes official vendor resources from a trusted organization.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

Signed by skilld at e50044c. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 5 months ago
What it can do
Runs commands
metadata
{
  "author": "apollographql",
  "version": "1.0.0"
}
All 1 allowed tools
Bash(apollo-ios-cli:*) Bash(swift:*) Bash(xcodebuild:*) Bash(git:*) Read Write Edit Glob Grep WebFetch
Other metadata
compatibility
iOS 15+, macOS 12+, tvOS 15+, watchOS 8+, visionOS 1+. Swift 6.1+, Xcode 16+. SwiftUI apps using Swift Concurrency.
  • graphql
  • swift
  • ios
  • apollo-client
  • code-generation
  • swiftui
  • async-await
  • caching
  • interceptors
  • websocket

README badge

README badge for apollographql/skills/apollo-ios

Generates strongly-typed Swift code from GraphQL operations and schemas, then configures an async/await ApolloClient with caching, interceptors, and WebSocket subscriptions for iOS 15+ and macOS 12+. Used to add Apollo iOS to Xcode projects via Swift Package Manager, wire code generation into builds, and implement queries and mutations from SwiftUI views.

Generated from the current SKILL.md.

What Apple platforms and Swift versions does Apollo iOS support?
Apollo iOS requires iOS 15+, macOS 12+, tvOS 15+, watchOS 8+, visionOS 1+, Swift 6.1+, and Xcode 16+. It targets SwiftUI apps using Swift Concurrency.
How do I install Apollo iOS in my project?
Add Apollo iOS via Swift Package Manager and install the `apollo-ios-cli`. Link the appropriate product (`Apollo` for targets using `ApolloClient`, `ApolloAPI` for targets only consuming generated models) to each target.
Should I run codegen from an Xcode build phase?
No. Do not wire `apollo-ios-cli generate` into an Xcode Run Script build phase — it slows compile times measurably on every build. Regenerate manually or via a dedicated script alias, and commit generated Swift files to source control.
How do I handle authentication with Apollo iOS?
Implement auth in a single `GraphQLInterceptor` that attaches tokens via `request.additionalHeaders["Authorization"]`, detects 401 responses via `.mapErrors`, and triggers retry by throwing `RequestChain.Retry(request:)`. Always pair it with `MaxRetryInterceptor` as a safety cap.
When should I enable test mocks?
Generate test mocks lazily by flipping `output.testMocks` from `none` to `swiftPackage` (or `absolute`) only when writing the first test that needs `Mock<Type>`, then regenerate and link the mocks target to your test target.

Generated from the current SKILL.md. These answers refresh after source changes.