Tüm Yazılar
KategoriNetworking
Okuma Süresi
22 dk
Yayın Tarihi
2024-04-20
Kelime Sayısı
2.115kelime

Kahveni hazırla - bu içerikli bir makale!

Network Layer Optimization: Production-Ready API Katmanı Oluşturma Rehberi

Özet

URLSession, async/await, retry logic, caching stratejileri ve offline-first yaklaşımı ile profesyonel network katmanı oluşturun. Gerçek dünya senaryoları ve best practices.

  • NetworkService actor yapisinda URLCache (50MB memory/100MB disk) ve custom JSONDecoder konfigure ediliyor.
  • RetryableNetworkService exponential backoff + jitter uygular, rate-limited'de Retry-After bekler.
  • CacheManager actor memory+disk cache katmanlariyla TTL bazli (varsayilan 300sn) veri saklama yapiyor.
  • Networking kutuphaneleri karsilastirmasinda URLSession 0KB, Alamofire ~250KB, Moya ~400KB boyutunda.
Network Layer Optimization: Production-Ready API Katmanı Oluşturma Rehberi

Mobil uygulamalarda network katmanı, kullanıcı deneyiminin temel taşlarından biridir. Yavaş API yanıtları, bağlantı kopmaları ve veri tutarsızlıkları kullanıcıları kaybetmenize neden olabilir. Bu kapsamlı rehberde, production-ready bir network layer nasıl oluşturulur A'dan Z'ye inceleyeceğiz.


İçindekiler


Modern Network Layer Mimarisi

swift
1// MARK: - Endpoint Protocol
2protocol Endpoint {
3 var baseURL: URL { get }
4 var path: String { get }
5 var method: HTTPMethod { get }
6 var headers: [String: String] { get }
7 var parameters: Parameters? { get }
8 var encoding: ParameterEncoding { get }
9}
10 
11enum HTTPMethod: String {
12 case get = "GET"
13 case post = "POST"
14 case put = "PUT"
15 case patch = "PATCH"
16 case delete = "DELETE"
17}
18 
19enum Parameters {
20 case body([String: Any])
21 case query([String: String])
22}
23 
24enum ParameterEncoding {
25 case json
26 case url
27}
28 
29// MARK: - API Endpoints
30enum ProductAPI: Endpoint {
31 case list(page: Int, limit: Int)
32 case detail(id: UUID)
33 case create(product: ProductRequest)
34 case update(id: UUID, product: ProductRequest)
35 case delete(id: UUID)
36 case search(query: String, filters: ProductFilters?)
37
38 var baseURL: URL {
39 URL(string: "https://api.myapp.com/v1")!
40 }
41
42 var path: String {
43 switch self {
44 case .list: return "/products"
45 case .detail(let id): return "/products/\(id)"
46 case .create: return "/products"
47 case .update(let id, _): return "/products/\(id)"
48 case .delete(let id): return "/products/\(id)"
49 case .search: return "/products/search"
50 }
51 }
52
53 var method: HTTPMethod {
54 switch self {
55 case .list, .detail, .search: return .get
56 case .create: return .post
57 case .update: return .put
58 case .delete: return .delete
59 }
60 }
61
62 var headers: [String: String] {
63 var headers = [
64 "Content-Type": "application/json",
65 "Accept": "application/json",
66 "X-API-Version": "1.0"
67 ]
68 if let token = AuthManager.shared.accessToken {
69 headers["Authorization"] = "Bearer \(token)"
70 }
71 return headers
72 }
73
74 var parameters: Parameters? {
75 switch self {
76 case .list(let page, let limit):
77 return .query(["page": "\(page)", "limit": "\(limit)"])
78 case .create(let product), .update(_, let product):
79 return .body(product.dictionary)
80 case .search(let query, let filters):
81 var params = ["q": query]
82 if let filters = filters {
83 params.merge(filters.queryParams) { _, new in new }
84 }
85 return .query(params)
86 default:
87 return nil
88 }
89 }
90
91 var encoding: ParameterEncoding {
92 switch method {
93 case .get: return .url
94 default: return .json
95 }
96 }
97}

Generic Network Service

swift
1// MARK: - Network Service Protocol
2protocol NetworkServiceProtocol {
3 func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
4 func request(_ endpoint: Endpoint) async throws -> Data
5}
6 
7// MARK: - Network Errors
8enum NetworkError: LocalizedError {
9 case invalidURL
10 case noData
11 case decodingError(Error)
12 case serverError(statusCode: Int, message: String?)
13 case unauthorized
14 case notFound
15 case rateLimited(retryAfter: TimeInterval?)
16 case noConnection
17 case timeout
18 case unknown(Error)
19
20 var errorDescription: String? {
21 switch self {
22 case .invalidURL: return "Geçersiz URL"
23 case .noData: return "Veri alınamadı"
24 case .decodingError(let error): return "Veri işlenemedi: \(error.localizedDescription)"
25 case .serverError(let code, let message): return "Sunucu hatası (\(code)): \(message ?? "Bilinmeyen hata")"
26 case .unauthorized: return "Oturum süresi doldu"
27 case .notFound: return "İçerik bulunamadı"
28 case .rateLimited: return "Çok fazla istek gönderildi"
29 case .noConnection: return "İnternet bağlantısı yok"
30 case .timeout: return "İstek zaman aşımına uğradı"
31 case .unknown(let error): return error.localizedDescription
32 }
33 }
34}
35 
36// MARK: - Network Service Implementation
37actor NetworkService: NetworkServiceProtocol {
38 static let shared = NetworkService()
39
40 private let session: URLSession
41 private let decoder: JSONDecoder
42 private let cache: URLCache
43
44 private init() {
45 // Custom URLSession configuration
46 let config = URLSessionConfiguration.default
47 config.timeoutIntervalForRequest = 30
48 config.timeoutIntervalForResource = 60
49 config.waitsForConnectivity = true
50 config.requestCachePolicy = .returnCacheDataElseLoad
51
52 // Cache configuration (50MB memory, 100MB disk)
53 cache = URLCache(
54 memoryCapacity: 50 * 1024 * 1024,
55 diskCapacity: 100 * 1024 * 1024
56 )
57 config.urlCache = cache
58
59 session = URLSession(configuration: config)
60
61 // JSON Decoder configuration
62 decoder = JSONDecoder()
63 decoder.keyDecodingStrategy = .convertFromSnakeCase
64 decoder.dateDecodingStrategy = .iso8601
65 }
66
67 func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
68 let data = try await request(endpoint)
69
70 do {
71 return try decoder.decode(T.self, from: data)
72 } catch {
73 throw NetworkError.decodingError(error)
74 }
75 }
76
77 func request(_ endpoint: Endpoint) async throws -> Data {
78 let request = try buildRequest(from: endpoint)
79
80 do {
81 let (data, response) = try await session.data(for: request)
82
83 guard let httpResponse = response as? HTTPURLResponse else {
84 throw NetworkError.unknown(NSError(domain: "Invalid response", code: -1))
85 }
86
87 try validateResponse(httpResponse, data: data)
88
89 return data
90 } catch let error as NetworkError {
91 throw error
92 } catch let error as URLError {
93 throw mapURLError(error)
94 } catch {
95 throw NetworkError.unknown(error)
96 }
97 }
98
99 private func buildRequest(from endpoint: Endpoint) throws -> URLRequest {
100 var urlComponents = URLComponents(url: endpoint.baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: true)
101
102 // Query parameters
103 if case .query(let params) = endpoint.parameters {
104 urlComponents?.queryItems = params.map { URLQueryItem(name: $0.key, value: $0.value) }
105 }
106
107 guard let url = urlComponents?.url else {
108 throw NetworkError.invalidURL
109 }
110
111 var request = URLRequest(url: url)
112 request.httpMethod = endpoint.method.rawValue
113 request.allHTTPHeaderFields = endpoint.headers
114
115 // Body parameters
116 if case .body(let params) = endpoint.parameters {
117 request.httpBody = try JSONSerialization.data(withJSONObject: params)
118 }
119
120 return request
121 }
122
123 private func validateResponse(_ response: HTTPURLResponse, data: Data) throws {
124 switch response.statusCode {
125 case 200...299:
126 return
127 case 401:
128 throw NetworkError.unauthorized
129 case 404:
130 throw NetworkError.notFound
131 case 429:
132 let retryAfter = response.value(forHTTPHeaderField: "Retry-After")
133 .flatMap { TimeInterval($0) }
134 throw NetworkError.rateLimited(retryAfter: retryAfter)
135 case 500...599:
136 let message = try? JSONDecoder().decode(ErrorResponse.self, from: data).message
137 throw NetworkError.serverError(statusCode: response.statusCode, message: message)
138 default:
139 throw NetworkError.serverError(statusCode: response.statusCode, message: nil)
140 }
141 }
142
143 private func mapURLError(_ error: URLError) -> NetworkError {
144 switch error.code {
145 case .notConnectedToInternet, .networkConnectionLost:
146 return .noConnection
147 case .timedOut:
148 return .timeout
149 default:
150 return .unknown(error)
151 }
152 }
153}

Retry Logic with Exponential Backoff

swift
1actor RetryableNetworkService {
2 private let networkService: NetworkServiceProtocol
3 private let maxRetries: Int
4 private let baseDelay: TimeInterval
5
6 init(
7 networkService: NetworkServiceProtocol = NetworkService.shared,
8 maxRetries: Int = 3,
9 baseDelay: TimeInterval = 1.0
10 ) {
11 self.networkService = networkService
12 self.maxRetries = maxRetries
13 self.baseDelay = baseDelay
14 }
15
16 func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
17 var lastError: Error?
18
19 for attempt in 0..<maxRetries {
20 do {
21 return try await networkService.request(endpoint)
22 } catch let error as NetworkError {
23 lastError = error
24
25 // Non-retryable errors
26 switch error {
27 case .unauthorized, .notFound, .decodingError:
28 throw error
29 case .rateLimited(let retryAfter):
30 if let delay = retryAfter {
31 try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
32 }
33 default:
34 // Exponential backoff
35 let delay = baseDelay * pow(2.0, Double(attempt))
36 let jitter = Double.random(in: 0...0.5)
37 try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
38 }
39 }
40 }
41
42 throw lastError ?? NetworkError.unknown(NSError(domain: "Max retries exceeded", code: -1))
43 }
44}

Caching Strategy

swift
1// MARK: - Cache Manager
2actor CacheManager {
3 static let shared = CacheManager()
4
5 private var memoryCache: [String: CacheEntry] = [:]
6 private let fileManager = FileManager.default
7 private let cacheDirectory: URL
8
9 private init() {
10 cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
11 .appendingPathComponent("NetworkCache")
12
13 try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
14 }
15
16 func get<T: Decodable>(_ key: String) async -> T? {
17 // Memory cache first
18 if let entry = memoryCache[key],
19 entry.expiresAt > Date(),
20 let value = entry.value as? T {
21 return value
22 }
23
24 // Disk cache
25 let fileURL = cacheDirectory.appendingPathComponent(key.sha256)
26 guard let data = try? Data(contentsOf: fileURL),
27 let cached = try? JSONDecoder().decode(CachedItem<T>.self, from: data),
28 cached.expiresAt > Date() else {
29 return nil
30 }
31
32 // Populate memory cache
33 memoryCache[key] = CacheEntry(value: cached.value, expiresAt: cached.expiresAt)
34
35 return cached.value
36 }
37
38 func set<T: Encodable>(_ value: T, for key: String, ttl: TimeInterval = 300) async {
39 let expiresAt = Date().addingTimeInterval(ttl)
40
41 // Memory cache
42 memoryCache[key] = CacheEntry(value: value, expiresAt: expiresAt)
43
44 // Disk cache
45 let cached = CachedItem(value: value, expiresAt: expiresAt)
46 if let data = try? JSONEncoder().encode(cached) {
47 let fileURL = cacheDirectory.appendingPathComponent(key.sha256)
48 try? data.write(to: fileURL)
49 }
50 }
51
52 func invalidate(_ key: String) async {
53 memoryCache.removeValue(forKey: key)
54 let fileURL = cacheDirectory.appendingPathComponent(key.sha256)
55 try? fileManager.removeItem(at: fileURL)
56 }
57
58 func clearAll() async {
59 memoryCache.removeAll()
60 try? fileManager.removeItem(at: cacheDirectory)
61 try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
62 }
63}
64 
65private struct CacheEntry {
66 let value: Any
67 let expiresAt: Date
68}
69 
70private struct CachedItem<T: Codable>: Codable {
71 let value: T
72 let expiresAt: Date
73}

Offline-First Approach

swift
1// MARK: - Repository with Offline Support
2class ProductRepository {
3 private let networkService: NetworkServiceProtocol
4 private let cacheManager: CacheManager
5 private let reachability: NetworkReachability
6
7 init(
8 networkService: NetworkServiceProtocol = NetworkService.shared,
9 cacheManager: CacheManager = .shared,
10 reachability: NetworkReachability = .shared
11 ) {
12 self.networkService = networkService
13 self.cacheManager = cacheManager
14 self.reachability = reachability
15 }
16
17 func getProducts(page: Int = 1) async throws -> [Product] {
18 let cacheKey = "products_page_\(page)"
19
20 // Offline: return cached data
21 if !reachability.isConnected {
22 if let cached: [Product] = await cacheManager.get(cacheKey) {
23 return cached
24 }
25 throw NetworkError.noConnection
26 }
27
28 // Online: fetch and cache
29 do {
30 let response: ProductListResponse = try await networkService.request(
31 ProductAPI.list(page: page, limit: 20)
32 )
33 await cacheManager.set(response.products, for: cacheKey, ttl: 600)
34 return response.products
35 } catch {
36 // Fallback to cache on error
37 if let cached: [Product] = await cacheManager.get(cacheKey) {
38 return cached
39 }
40 throw error
41 }
42 }
43}

Request Interceptors

Request'leri manipüle etmek için interceptor pattern:

swift
1// MARK: - Request Interceptor
2protocol RequestInterceptor {
3 func intercept(_ request: inout URLRequest) async throws
4}
5 
6// Auth Token Interceptor
7class AuthInterceptor: RequestInterceptor {
8 func intercept(_ request: inout URLRequest) async throws {
9 guard let token = await AuthManager.shared.getValidToken() else {
10 throw NetworkError.unauthorized
11 }
12 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
13 }
14}
15 
16// Logging Interceptor
17class LoggingInterceptor: RequestInterceptor {
18 func intercept(_ request: inout URLRequest) async throws {
19 #if DEBUG
20 print("🌐 [\(Date())] \(request.httpMethod ?? "?") \(request.url?.absoluteString ?? "?")")
21 #endif
22 }
23}
24 
25// Interceptor Chain
26class InterceptorChain {
27 private var interceptors: [RequestInterceptor] = []
28
29 func add(_ interceptor: RequestInterceptor) {
30 interceptors.append(interceptor)
31 }
32
33 func process(_ request: inout URLRequest) async throws {
34 for interceptor in interceptors {
35 try await interceptor.intercept(&request)
36 }
37 }
38}

Network Monitoring

swift
1import Network
2 
3actor NetworkMonitor {
4 static let shared = NetworkMonitor()
5 private let monitor = NWPathMonitor()
6
7 private(set) var isConnected = true
8 private(set) var connectionType: ConnectionType = .unknown
9
10 enum ConnectionType { case wifi, cellular, ethernet, unknown }
11
12 func startMonitoring() {
13 monitor.pathUpdateHandler = { [weak self] path in
14 Task { await self?.updatePath(path) }
15 }
16 monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))
17 }
18
19 private func updatePath(_ path: NWPath) {
20 isConnected = path.status == .satisfied
21
22 if path.usesInterfaceType(.wifi) { connectionType = .wifi }
23 else if path.usesInterfaceType(.cellular) { connectionType = .cellular }
24 else { connectionType = .unknown }
25
26 NotificationCenter.default.post(name: .networkStatusChanged, object: nil)
27 }
28}
29 
30extension Notification.Name {
31 static let networkStatusChanged = Notification.Name("networkStatusChanged")
32}

Networking Kütüphaneleri Karşılaştırması

Özellik
URLSession (Native)
Alamofire
Moya
Kurulum
Ek bağımlılık yok
SPM/CocoaPods
Alamofire üzerine
Boyut
0 KB (built-in)
~250 KB
~400 KB
Async/Await
iOS 15+ native
v5.5+ wrapper
Destekliyor
Interceptors
Manuel
RequestInterceptor protocol
Plugin sistemi
Cache
URLCache (built-in)
ResponseCacher
Moya Plugin
Retry Logic
Manuel implementasyon
RequestRetrier protocol
Built-in
Type Safety
Orta
Orta
Yüksek (TargetType)
Öğrenme Eğrisi
Düşük
Orta
Yüksek
💡 Altın İpucu: URLSession'ın multipathServiceType = .handover ayarını kullanarak Wi-Fi'den cellular'a geçişlerde bağlantı kopmasını önleyebilirsiniz. Kullanıcı Wi-Fi kapsama alanından çıkarken aktif indirme veya streaming işlemleri kesintisiz devam eder. Bu özellik büyük dosya transferleri ve real-time uygulamalar için kritiktir.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Sonuç ve Öneriler

Key Takeaways

  • ✅ Type-safe Endpoints - Compile-time hata yakalama
  • ✅ Generic Services - DRY, maintainable kod
  • ✅ Retry + Backoff - Geçici hatalar için resilience
  • ✅ Smart Caching - Performans ve offline deneyim
  • ✅ Error Handling - User-friendly, actionable hatalar

Kaynaklar

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

Etiketler

#Networking#URLSession#iOS#API#HTTP#Performance
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik