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

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

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.

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ı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

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

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz