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.

referencesoperations.md

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

Operations

This reference covers queries, mutations, watchers, cache policies, error handling, and SwiftUI @Observable view-model patterns.

Apollo iOS v2 uses async/await and typed cache policies. Every operation returns a GraphQLResponse<Operation> that carries data, errors, and source (cache vs server).

Write operations in .graphql files

Operations live in .graphql files inside a path listed in input.operationSearchPaths of apollo-codegen-config.json. Name each file after the operation it contains and include the operation name explicitly so the generated type is predictable.

# GetUserQuery.graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
  }
}

Keep one operation per file. Co-locate fragments that are specific to an operation; put shared fragments in their own .graphql file.

After editing a .graphql file, regenerate Swift types:

./apollo-ios-cli generate

Queries

ApolloClient.fetch(query:cachePolicy:) returns a single GraphQLResponse<Query> when the cache policy produces one response, and an AsyncThrowingStream when it produces more than one.

Single response — cacheFirst (default), networkFirst, or networkOnly

import Apollo

func loadUser(id: String) async throws -> GetUserQuery.Data.User? {
  let response = try await apolloClient.fetch(
    query: GetUserQuery(id: id),
    cachePolicy: .cacheFirst
  )
  if let firstError = response.errors?.first {
    // `GraphQLError` conforms to `Error`, so it can be thrown directly.
    // Inspect `response.errors` for all of them if you need the full list.
    throw firstError
  }
  return response.data?.user
}

Two responses — cacheAndNetwork

Returns cached data first (if any), then the network result. Use this when a view should render quickly from cache and then refresh.

func streamUser(id: String) throws -> AsyncThrowingStream<GraphQLResponse<GetUserQuery>, any Error> {
  try apolloClient.fetch(query: GetUserQuery(id: id), cachePolicy: .cacheAndNetwork)
}

Cache-only — cacheOnly

Returns GraphQLResponse<Query>? — nil if the cache has no data.

let cached = try await apolloClient.fetch(query: GetUserQuery(id: id), cachePolicy: .cacheOnly)

Cache policies

Policy Enum case Return type When to use
Cache first CachePolicy.Query.SingleResponse.cacheFirst GraphQLResponse<Query> Default. Serve cache hits instantly; fall back to network on miss.
Network first CachePolicy.Query.SingleResponse.networkFirst GraphQLResponse<Query> Correctness-critical reads (e.g. a checkout page).
Network only CachePolicy.Query.SingleResponse.networkOnly GraphQLResponse<Query> Pull-to-refresh, or when cache is known stale.
Cache only CachePolicy.Query.CacheOnly.cacheOnly GraphQLResponse<Query>? Offline reads, or checking what's already in cache.
Cache + network CachePolicy.Query.CacheAndNetwork.cacheAndNetwork AsyncThrowingStream<GraphQLResponse<Query>, Error> Show cached UI instantly, then update once network returns.

Note: The legacy CachePolicy_v1 enum (returnCacheDataElseFetch, fetchIgnoringCacheData, etc.) is deprecated. Use the typed CachePolicy.Query.* variants shown above.

Mutations

perform(mutation:) always hits the network (mutations have no cache policy). The return value is a GraphQLResponse<Mutation>.

func updateUserName(id: String, name: String) async throws {
  let response = try await apolloClient.perform(
    mutation: UpdateUserNameMutation(id: id, name: name)
  )
  if let firstError = response.errors?.first {
    throw firstError
  }
}

When the mutation's selection set matches the shape of cached records, the cache updates automatically. For optimistic UI or cross-entity updates, see caching.md.

Watchers

watch(query:cachePolicy:resultHandler:) returns a GraphQLQueryWatcher<Query> that fires the handler every time the matched records in the cache change. Watchers are the reactive primitive for SwiftUI — use them instead of polling.

The handler is a closure, not an AsyncSequence:

public typealias ResultHandler = @Sendable (Result<GraphQLResponse<Query>, any Swift.Error>) -> Void

Bridging a watcher to an @State variable for SwiftUI

Store the watcher for the lifetime of the view and tear it down on cancellation:

import Apollo

@Observable
@MainActor
final class UserViewModel {
  var user: GetUserQuery.Data.User?
  var errorMessage: String?

  private let apolloClient: ApolloClient
  private var watcher: GraphQLQueryWatcher<GetUserQuery>?

  init(apolloClient: ApolloClient) { self.apolloClient = apolloClient }

  func start(userID: String) async {
    cancel()
    watcher = await apolloClient.watch(
      query: GetUserQuery(id: userID),
      cachePolicy: .cacheFirst
    ) { [weak self] result in
      Task { @MainActor in
        guard let self else { return }
        switch result {
        case .success(let response):
          self.user = response.data?.user
          self.errorMessage = response.errors?.first?.message
        case .failure(let error):
          self.errorMessage = error.localizedDescription
        }
      }
    }
  }

  func cancel() {
    watcher?.cancel()
    watcher = nil
  }

  deinit { watcher?.cancel() }
}

Consuming from a SwiftUI view

struct UserDetailView: View {
  let userID: String
  @State private var viewModel: UserViewModel

  init(userID: String, apolloClient: ApolloClient) {
    self.userID = userID
    _viewModel = State(initialValue: UserViewModel(apolloClient: apolloClient))
  }

  var body: some View {
    Group {
      if let user = viewModel.user {
        Text(user.name)
      } else if let message = viewModel.errorMessage {
        Text(message).foregroundStyle(.red)
      } else {
        ProgressView()
      }
    }
    .task(id: userID) {
      await viewModel.start(userID: userID)
    }
    .onDisappear {
      viewModel.cancel()
    }
  }
}

Use .task(id:) so the watcher restarts whenever userID changes. .task cancels automatically when the view disappears, but watchers require an explicit cancel() because they are not bound to a Swift Task.

Error handling

A network response can succeed (no thrown error) while still containing GraphQL errors in response.errors. Always check both.

let response = try await apolloClient.fetch(query: GetUserQuery(id: id))

// `response.errors` is `[GraphQLError]?`. Each `GraphQLError` conforms to
// `Error` and carries `.message`, `.locations`, `.path`, and `.extensions`.
// `response.data` may still contain partial data when there are errors.
if let firstError = response.errors?.first {
  throw firstError
}

guard let user = response.data?.user else {
  // No user was returned but no errors either — treat as not found.
  throw UserNotFound()
}

If you need to propagate all GraphQL errors (not just the first) and don't want to lose the rest, wrap them in an app-owned error type — for example:

enum APIError: Error {
  case graphQL([GraphQLError])
}

if let errors = response.errors, !errors.isEmpty {
  throw APIError.graphQL(errors)
}

APIError here is an app-level type you define and name to match your codebase — it is not provided by Apollo iOS. Apollo iOS ships only the singular GraphQLError; any aggregation wrapper is yours to design.

Errors that prevent a response entirely (network failures, cancellations, parsing errors) are thrown from fetch / perform / subscribe and surface as Swift.Error. Check specifically for CancellationError when an async Task is cancelled.

response.source tells you where data came from — .cache or .server. Useful for analytics or deciding whether to trigger a refresh.

Ground rules

  • Use .task { } / .task(id:) to scope fetch Tasks to view lifetime so they cancel automatically.
  • Cancel watchers explicitly; they are not bound to a Task.
  • Create view models as @MainActor @Observable classes and hand them the ApolloClient at init. Do not fetch from inside body.
  • Only select fields the UI actually uses. Every extra field is a larger cache record and a larger payload.
  • Treat response.errors as data, not an exception. Partial responses are common in federated schemas.
  • Never share a GraphQLQueryWatcher across views; each view owns its own watcher.

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.