ios-graphql/SKILL.md
Implement GraphQL in iOS apps with Apollo Client, code generation, caching, and optimistic updates. Use when building GraphQL APIs, managing queries/mutations, implementing subscriptions, or optimizing network requests. Triggers on GraphQL, Apollo, query, mutation, subscription, schema, fragment, cache, codegen, gql.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-graphqlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert in GraphQL for iOS. When this skill activates, help implement efficient GraphQL clients.
// Package.swift dependencies
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0")
// Target dependencies
.product(name: "Apollo", package: "apollo-ios"),
.product(name: "ApolloAPI", package: "apollo-ios"),
.product(name: "ApolloWebSocket", package: "apollo-ios"), // For subscriptions
# Install Apollo CLI
brew install apollo-ios-cli
# Initialize
apollo-ios-cli init --schema-namespace MySchema
# Download schema
apollo-ios-cli fetch-schema \
--endpoint-url https://api.example.com/graphql
# Generate code
apollo-ios-cli generate
{
"schemaNamespace": "MySchema",
"input": {
"operationSearchPaths": ["**/*.graphql"],
"schemaSearchPaths": ["schema.graphqls"]
},
"output": {
"testMocks": {
"swiftPackage": {
"targetName": "MySchemaTestMocks"
}
},
"schemaTypes": {
"path": "./MySchema",
"moduleType": {
"swiftPackageManager": {}
}
},
"operations": {
"inSchemaModule": {}
}
}
}
import Apollo
class GraphQLClient {
static let shared = GraphQLClient()
private(set) lazy var apollo: ApolloClient = {
let url = URL(string: "https://api.example.com/graphql")!
return ApolloClient(url: url)
}()
}
import Apollo
import ApolloAPI
class GraphQLClient {
static let shared = GraphQLClient()
private(set) lazy var apollo: ApolloClient = {
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let provider = NetworkInterceptorProvider(
store: store,
client: URLSessionClient()
)
let url = URL(string: "https://api.example.com/graphql")!
let transport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url
)
return ApolloClient(networkTransport: transport, store: store)
}()
}
// Custom interceptor for auth
class AuthorizationInterceptor: ApolloInterceptor {
var id: String = "AuthorizationInterceptor"
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
if let token = TokenManager.shared.accessToken {
request.addHeader(name: "Authorization", value: "Bearer \(token)")
}
chain.proceedAsync(
request: request,
response: response,
interceptor: self,
completion: completion
)
}
}
// Interceptor provider
class NetworkInterceptorProvider: DefaultInterceptorProvider {
override func interceptors<Operation: GraphQLOperation>(
for operation: Operation
) -> [ApolloInterceptor] {
var interceptors = super.interceptors(for: operation)
interceptors.insert(AuthorizationInterceptor(), at: 0)
return interceptors
}
}
# GetUser.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
avatar
posts {
id
title
}
}
}
# GetUsers.graphql with pagination
query GetUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
# CreateUser.graphql
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
name
email
}
errors {
field
message
}
}
}
# UpdateProfile.graphql
mutation UpdateProfile($input: UpdateProfileInput!) {
updateProfile(input: $input) {
success
user {
...UserDetails
}
}
}
# UserDetails.graphql
fragment UserDetails on User {
id
name
email
avatar
createdAt
}
# Use in queries
query GetCurrentUser {
me {
...UserDetails
settings {
notifications
theme
}
}
}
# OnMessageReceived.graphql
subscription OnMessageReceived($channelId: ID!) {
messageReceived(channelId: $channelId) {
id
content
sender {
id
name
}
createdAt
}
}
import Apollo
class UserRepository {
private let client = GraphQLClient.shared.apollo
func fetchUser(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
client.fetch(query: GetUserQuery(id: id)) { result in
switch result {
case .success(let graphQLResult):
if let user = graphQLResult.data?.user {
continuation.resume(returning: User(from: user))
} else if let errors = graphQLResult.errors {
continuation.resume(throwing: GraphQLError.queryFailed(errors))
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// With cache policy
func fetchUserCacheFirst(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
client.fetch(
query: GetUserQuery(id: id),
cachePolicy: .returnCacheDataElseFetch
) { result in
// Handle result
}
}
}
}
func createUser(name: String, email: String) async throws -> User {
let input = CreateUserInput(name: name, email: email)
return try await withCheckedThrowingContinuation { continuation in
client.perform(mutation: CreateUserMutation(input: input)) { result in
switch result {
case .success(let graphQLResult):
if let user = graphQLResult.data?.createUser?.user {
continuation.resume(returning: User(from: user))
} else if let errors = graphQLResult.data?.createUser?.errors {
continuation.resume(throwing: GraphQLError.validation(errors))
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
import ApolloWebSocket
class MessageSubscription {
private var subscription: Cancellable?
func subscribe(channelId: String, onMessage: @escaping (Message) -> Void) {
let client = GraphQLClient.shared.apollo
subscription = client.subscribe(
subscription: OnMessageReceivedSubscription(channelId: channelId)
) { result in
switch result {
case .success(let graphQLResult):
if let message = graphQLResult.data?.messageReceived {
onMessage(Message(from: message))
}
case .failure(let error):
print("Subscription error: \(error)")
}
}
}
func unsubscribe() {
subscription?.cancel()
subscription = nil
}
}
// WebSocket setup for subscriptions
class GraphQLClient {
private(set) lazy var apollo: ApolloClient = {
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
// HTTP transport for queries/mutations
let httpTransport = RequestChainNetworkTransport(
interceptorProvider: NetworkInterceptorProvider(store: store, client: URLSessionClient()),
endpointURL: URL(string: "https://api.example.com/graphql")!
)
// WebSocket transport for subscriptions
let webSocketClient = WebSocket(
url: URL(string: "wss://api.example.com/graphql")!,
protocol: .graphql_ws
)
let webSocketTransport = WebSocketTransport(
websocket: webSocketClient,
config: WebSocketTransport.Configuration(
reconnect: true,
connectingPayload: ["authToken": TokenManager.shared.accessToken ?? ""]
)
)
// Split transport
let splitTransport = SplitNetworkTransport(
uploadingNetworkTransport: httpTransport,
webSocketNetworkTransport: webSocketTransport
)
return ApolloClient(networkTransport: splitTransport, store: store)
}()
}
// Available policies
.fetchIgnoringCacheData // Always fetch from network
.fetchIgnoringCacheCompletely // Fetch, don't update cache
.returnCacheDataElseFetch // Cache first, then network
.returnCacheDataDontFetch // Cache only
.returnCacheDataAndFetch // Cache immediately, then network
// Usage
client.fetch(
query: GetUserQuery(id: id),
cachePolicy: .returnCacheDataAndFetch
) { result in
// Called twice if cache has data: once with cache, once with network
}
// Update cache after mutation
client.perform(mutation: CreatePostMutation(input: input)) { [weak self] result in
guard case .success(let graphQLResult) = result,
let post = graphQLResult.data?.createPost?.post else { return }
// Update cache
self?.client.store.withinReadWriteTransaction { transaction in
// Add to list
try transaction.update(query: GetPostsQuery()) { (data: inout GetPostsQuery.Data) in
data.posts.append(post)
}
}
}
// Show result before server confirms
client.perform(
mutation: LikePostMutation(postId: postId),
publishResultToStore: true
) { result in
// Handle result
}
// Manual optimistic update
func likePost(postId: String) {
// 1. Optimistically update UI
updateLocalLikeState(postId: postId, liked: true)
// 2. Perform mutation
client.perform(mutation: LikePostMutation(postId: postId)) { [weak self] result in
switch result {
case .success:
break // Already updated optimistically
case .failure:
// Rollback
self?.updateLocalLikeState(postId: postId, liked: false)
}
}
}
@MainActor
@Observable
class UserViewModel {
private let repository: UserRepository
var user: User?
var isLoading = false
var error: Error?
init(repository: UserRepository = UserRepository()) {
self.repository = repository
}
func loadUser(id: String) async {
isLoading = true
error = nil
do {
user = try await repository.fetchUser(id: id)
} catch {
self.error = error
}
isLoading = false
}
}
// View
struct UserProfileView: View {
@State private var viewModel = UserViewModel()
let userId: String
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else if let user = viewModel.user {
UserContent(user: user)
} else if let error = viewModel.error {
ErrorView(error: error, retry: { Task { await viewModel.loadUser(id: userId) } })
}
}
.task {
await viewModel.loadUser(id: userId)
}
}
}
@MainActor
@Observable
class UsersViewModel {
private let client = GraphQLClient.shared.apollo
var users: [User] = []
var isLoading = false
var hasNextPage = true
private var endCursor: String?
func loadUsers() async {
guard !isLoading, hasNextPage else { return }
isLoading = true
do {
let result = try await fetchUsers(first: 20, after: endCursor)
users.append(contentsOf: result.users)
hasNextPage = result.pageInfo.hasNextPage
endCursor = result.pageInfo.endCursor
} catch {
// Handle error
}
isLoading = false
}
func refresh() async {
users = []
endCursor = nil
hasNextPage = true
await loadUsers()
}
}
enum GraphQLError: LocalizedError {
case queryFailed([Apollo.GraphQLError])
case validation([ValidationError])
case unauthorized
case networkError(Error)
var errorDescription: String? {
switch self {
case .queryFailed(let errors):
return errors.map { $0.message ?? "Unknown error" }.joined(separator: ", ")
case .validation(let errors):
return errors.map { "\($0.field): \($0.message)" }.joined(separator: ", ")
case .unauthorized:
return "Session expired. Please login again."
case .networkError(let error):
return error.localizedDescription
}
}
}
// Error interceptor
class ErrorInterceptor: ApolloInterceptor {
var id: String = "ErrorInterceptor"
func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
chain.proceedAsync(request: request, response: response, interceptor: self) { result in
switch result {
case .success(let graphQLResult):
// Check for auth errors
if graphQLResult.errors?.contains(where: { $0.message?.contains("Unauthorized") == true }) == true {
NotificationCenter.default.post(name: .userSessionExpired, object: nil)
}
completion(.success(graphQLResult))
case .failure(let error):
completion(.failure(error))
}
}
}
}
import XCTest
@testable import MyApp
import Apollo
import ApolloTestSupport
import MySchemaTestMocks
final class UserViewModelTests: XCTestCase {
func test_loadUser_success() async {
// Given
let mock = Mock<Query>()
mock.user = Mock<User>(id: "1", name: "Test User", email: "[email protected]")
let mockClient = MockGraphQLClient(mock: mock)
let viewModel = UserViewModel(client: mockClient)
// When
await viewModel.loadUser(id: "1")
// Then
XCTAssertNotNil(viewModel.user)
XCTAssertEqual(viewModel.user?.name, "Test User")
}
}
// Use fragments for reusable fields
fragment UserBasic on User {
id
name
avatar
}
// Batch related data in one query
query GetDashboard {
me { ...UserBasic }
notifications(first: 5) { edges { node { id title } } }
recentPosts(first: 10) { edges { node { id title } } }
}
// Use cache wisely
client.fetch(query: query, cachePolicy: .returnCacheDataElseFetch)
// Handle partial data
if let user = result.data?.user {
// Use data
}
// Don't over-fetch
query GetUserWithEverything {
user(id: $id) {
...AllUserFields // Too much data
posts { ...AllPostFields }
comments { ...AllCommentFields }
}
}
// Don't ignore errors
client.fetch(query: query) { result in
let user = result.data?.user // ❌ Missing error handling
}
// Don't skip cache for frequently accessed data
cachePolicy: .fetchIgnoringCacheCompletely // ❌ Wasteful
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes