REST API ile çalışırken over-fetching (fazla veri çekme) ve under-fetching (eksik veri, N+1 request) sorunlarıyla karşılaşıyorsun. GraphQL, client'ın tam olarak ihtiyacı olan veriyi istemesini sağlar. Apollo iOS client ile Swift'te type-safe GraphQL kullanabilirsin.
💡 Hızlı Not: Apollo iOS 1.0+, Swift code generation ile tam tip güvenliği sağlar. Bu rehber Apollo iOS 1.x API'larını kapsar.
İçindekiler
- GraphQL vs REST
- Apollo iOS Kurulumu
- Schema ve Code Generation
- Query ve Mutation
- Caching Stratejileri
- Pagination
- Optimistic UI
- Subscriptions (Real-time)
- Error Handling
- Best Practices
GraphQL vs REST {#graphql-vs-rest}
Özellik | REST | GraphQL |
|---|---|---|
Veri alma | Fixed endpoints | Flexible queries |
Over-fetching | ✅ Sık | ❌ Yok |
Under-fetching | ✅ N+1 requests | ❌ Tek query |
Versiyonlama | /v1, /v2 | Schema evolution |
Caching | HTTP cache (basit) | Normalized cache |
Tip güvenliği | Manual | Otomatik (codegen) |
Tooling | Swagger/OpenAPI | GraphiQL, Apollo Studio |
Apollo iOS Kurulumu {#kurulum}
swift
1// Package.swift2dependencies: [3 .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0"),4]5// Target dependency: "Apollo", "ApolloWebSocket"swift
1// Apollo client setup2import Apollo3 4class Network {5 static let shared = Network()6 let apollo: ApolloClient7 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: url15 )16 apollo = ApolloClient(networkTransport: transport, store: store)17 }18}Schema ve Code Generation {#codegen}
graphql
1# Queries/GetProducts.graphql2query GetProducts($first: Int!, $after: String) {3 products(first: $first, after: $after) {4 edges {5 node {6 id7 name8 price9 category {10 id11 name12 }13 reviews {14 rating15 text16 }17 }18 }19 pageInfo {20 hasNextPage21 endCursor22 }23 }24}25 26# Mutations/CreateOrder.graphql27mutation CreateOrder($input: CreateOrderInput!) {28 createOrder(input: $input) {29 id30 status31 total32 items {33 product { name }34 quantity35 }36 }37}Code generation otomatik Swift tipleri oluşturur. apollo-ios-cli generate komutu ile çalıştırılır.
Query ve Mutation {#query-mutation}
swift
1// Query2func fetchProducts() async throws -> [GetProductsQuery.Data.Products.Edge.Node] {3 return try await withCheckedThrowingContinuation { continuation in4 Network.shared.apollo.fetch(5 query: GetProductsQuery(first: 20, after: nil),6 cachePolicy: .fetchIgnoringCacheCompletely7 ) { result in8 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// Mutation20func createOrder(items: [OrderItemInput]) async throws -> CreateOrderMutation.Data.CreateOrder {21 let input = CreateOrderInput(items: items)22 return try await withCheckedThrowingContinuation { continuation in23 Network.shared.apollo.perform(mutation: CreateOrderMutation(input: input)) { result in24 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 Stratejileri {#caching}
Apollo iOS normalized cache kullanır — her obje id ile depolanır ve otomatik güncellenir:
swift
1// Cache policy seçenekleri2.returnCacheDataElseFetch // Önce cache, yoksa network3.fetchIgnoringCacheData // Her zaman network4.returnCacheDataDontFetch // Sadece cache5.returnCacheDataAndFetch // Cache göster, arka planda güncellePagination {#pagination}
Cursor-based pagination ile sonsuz scroll:
swift
1class ProductListViewModel: ObservableObject {2 @Published var products: [ProductNode] = []3 private var endCursor: String?4 private var hasNextPage = true5 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.endCursor11 hasNextPage = result?.pageInfo.hasNextPage ?? false12 }13}Optimistic UI {#optimistic}
swift
1// Mutation sonucunu beklemeden UI'ı güncelle2Network.shared.apollo.perform(3 mutation: ToggleFavoriteMutation(productId: id),4 optimisticUpdate: { cache in5 // Cache'i hemen güncelle6 try? cache.updateObject(ofType: ProductNode.self, withKey: "Product:\(id)") { product in7 product.isFavorite = !product.isFavorite8 }9 }10)Subscriptions (Real-time) {#subscriptions}
swift
1// WebSocket ile real-time güncellemeler2let subscription = Network.shared.apollo.subscribe(3 subscription: OrderStatusSubscription(orderId: orderId)4) { result in5 switch result {6 case .success(let response):7 if let status = response.data?.orderStatusChanged.status {8 self.orderStatus = status9 }10 case .failure(let error):11 print("Subscription error: \(error)")12 }13}Error Handling {#error-handling}
GraphQL partial errors — bazı field'lar başarılı, bazıları hatalı olabilir:
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 // Path bilgisi hangi field'ın hatalı olduğunu gösterir6 print("Path: \(error.path ?? [])")7 }8 }9 if let data = result.data {10 // Partial data kullanılabilir11 self.products = data.products.edges.map { $0.node }12 }13}Fragment Kullanımı {#fragments}
Fragment'lar, tekrar eden field gruplarını paylaşmanı sağlar — DRY prensibini GraphQL'de uygular:
graphql
1# Fragments/ProductFields.graphql2fragment ProductFields on Product {3 id4 name5 price6 imageUrl7 category {8 id9 name10 }11}12 13# Fragments/UserFields.graphql14fragment UserFields on User {15 id16 name17 email18 avatar19}20 21# Queries/GetProductDetail.graphql22query GetProductDetail($id: ID!) {23 product(id: $id) {24 ...ProductFields25 description26 stock27 reviews {28 id29 rating30 text31 author {32 ...UserFields33 }34 }35 }36}37 38# Queries/GetCart.graphql39query GetCart {40 cart {41 items {42 quantity43 product {44 ...ProductFields45 }46 }47 totalPrice48 }49}Fragment kullanmanın avantajları:
Avantaj | Aciklama |
|---|---|
Kod tekrarini onler | Ayni field'lari her query'de yeniden yazmazsin |
Tip tutarliligi | Code generation ile fragment'lar ayri Swift struct olur |
Bakım kolayligi | Field eklemek/cikmak tek yerden yapilir |
Cache uyumu | Apollo normalized cache fragment'lari otomatik esler |
Custom Interceptor ile Auth Token {#interceptors}
Apollo iOS interceptor chain ile her request'e otomatik auth token ekleyebilirsin:
swift
1// AuthInterceptor.swift2class AuthInterceptor: ApolloInterceptor {3 let id = "AuthInterceptor"4 private let tokenProvider: TokenProvider5 6 init(tokenProvider: TokenProvider) {7 self.tokenProvider = tokenProvider8 }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>) -> Void15 ) {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 - debug icin26class 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>) -> Void34 ) {35 let operationName = Operation.operationName36 let startTime = CFAbsoluteTimeGetCurrent()37 print("[GraphQL] --> \(operationName)")38 39 chain.proceedAsync(request: request, response: response, interceptor: self) { result in40 let duration = CFAbsoluteTimeGetCurrent() - startTime41 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 InterceptorProvider53class NetworkInterceptorProvider: DefaultInterceptorProvider {54 let tokenProvider: TokenProvider55 56 init(store: ApolloStore, tokenProvider: TokenProvider) {57 self.tokenProvider = tokenProvider58 super.init(store: store)59 }60 61 override func interceptors<Operation: GraphQLOperation>(62 for operation: Operation63 ) -> [ApolloInterceptor] {64 var interceptors = super.interceptors(for: operation)65 interceptors.insert(AuthInterceptor(tokenProvider: tokenProvider), at: 0)66 interceptors.insert(LoggingInterceptor(), at: 0)67 return interceptors68 }69}Best Practices {#best-practices}
- Fragment kullan — tekrar eden field'ları paylaş
- Persisted queries — production'da query string gönderme, hash gönder
- Batch queries — birden fazla query'yi tek request'te gönder
- Code generation CI'da — her schema değişikliğinde otomatik generate
- Error boundary — partial data handling
- Interceptor chain — auth, logging, retry logic'i interceptor olarak yaz
- Fragment colocation — fragment'ları kullanan view'ın yanına koy
- Query complexity — derin nested query'lerden kaçın, server-side depth limit koy
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var: **Kaynaklar:** - [Apollo iOS Documentation](https://www.apollographql.com/docs/ios/) - [GraphQL Specification](https://spec.graphql.org/) - [GraphQL Best Practices](https://graphql.org/learn/best-practices/)

