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
- Generic Network Service
- Retry Logic with Exponential Backoff
- Caching Strategy
- Offline-First Approach
- Request Interceptors
- Network Monitoring
- Networking Kütüphaneleri Karşılaştırması
- Sonuç ve Öneriler
Modern Network Layer Mimarisi
swift
1// MARK: - Endpoint Protocol2protocol 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 json26 case url27}28 29// MARK: - API Endpoints30enum 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 .get56 case .create: return .post57 case .update: return .put58 case .delete: return .delete59 }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 headers72 }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 nil88 }89 }90 91 var encoding: ParameterEncoding {92 switch method {93 case .get: return .url94 default: return .json95 }96 }97}Generic Network Service
swift
1// MARK: - Network Service Protocol2protocol NetworkServiceProtocol {3 func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T4 func request(_ endpoint: Endpoint) async throws -> Data5}6 7// MARK: - Network Errors8enum NetworkError: LocalizedError {9 case invalidURL10 case noData11 case decodingError(Error)12 case serverError(statusCode: Int, message: String?)13 case unauthorized14 case notFound15 case rateLimited(retryAfter: TimeInterval?)16 case noConnection17 case timeout18 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.localizedDescription32 }33 }34}35 36// MARK: - Network Service Implementation37actor NetworkService: NetworkServiceProtocol {38 static let shared = NetworkService()39 40 private let session: URLSession41 private let decoder: JSONDecoder42 private let cache: URLCache43 44 private init() {45 // Custom URLSession configuration46 let config = URLSessionConfiguration.default47 config.timeoutIntervalForRequest = 3048 config.timeoutIntervalForResource = 6049 config.waitsForConnectivity = true50 config.requestCachePolicy = .returnCacheDataElseLoad51 52 // Cache configuration (50MB memory, 100MB disk)53 cache = URLCache(54 memoryCapacity: 50 * 1024 * 1024,55 diskCapacity: 100 * 1024 * 102456 )57 config.urlCache = cache58 59 session = URLSession(configuration: config)60 61 // JSON Decoder configuration62 decoder = JSONDecoder()63 decoder.keyDecodingStrategy = .convertFromSnakeCase64 decoder.dateDecodingStrategy = .iso860165 }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 data90 } catch let error as NetworkError {91 throw error92 } 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 parameters103 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.invalidURL109 }110 111 var request = URLRequest(url: url)112 request.httpMethod = endpoint.method.rawValue113 request.allHTTPHeaderFields = endpoint.headers114 115 // Body parameters116 if case .body(let params) = endpoint.parameters {117 request.httpBody = try JSONSerialization.data(withJSONObject: params)118 }119 120 return request121 }122 123 private func validateResponse(_ response: HTTPURLResponse, data: Data) throws {124 switch response.statusCode {125 case 200...299:126 return127 case 401:128 throw NetworkError.unauthorized129 case 404:130 throw NetworkError.notFound131 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).message137 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 .noConnection147 case .timedOut:148 return .timeout149 default:150 return .unknown(error)151 }152 }153}Retry Logic with Exponential Backoff
swift
1actor RetryableNetworkService {2 private let networkService: NetworkServiceProtocol3 private let maxRetries: Int4 private let baseDelay: TimeInterval5 6 init(7 networkService: NetworkServiceProtocol = NetworkService.shared,8 maxRetries: Int = 3,9 baseDelay: TimeInterval = 1.010 ) {11 self.networkService = networkService12 self.maxRetries = maxRetries13 self.baseDelay = baseDelay14 }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 = error24 25 // Non-retryable errors26 switch error {27 case .unauthorized, .notFound, .decodingError:28 throw error29 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 backoff35 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 Manager2actor CacheManager {3 static let shared = CacheManager()4 5 private var memoryCache: [String: CacheEntry] = [:]6 private let fileManager = FileManager.default7 private let cacheDirectory: URL8 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 first18 if let entry = memoryCache[key],19 entry.expiresAt > Date(),20 let value = entry.value as? T {21 return value22 }23 24 // Disk cache25 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 nil30 }31 32 // Populate memory cache33 memoryCache[key] = CacheEntry(value: cached.value, expiresAt: cached.expiresAt)34 35 return cached.value36 }37 38 func set<T: Encodable>(_ value: T, for key: String, ttl: TimeInterval = 300) async {39 let expiresAt = Date().addingTimeInterval(ttl)40 41 // Memory cache42 memoryCache[key] = CacheEntry(value: value, expiresAt: expiresAt)43 44 // Disk cache45 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: Any67 let expiresAt: Date68}69 70private struct CachedItem<T: Codable>: Codable {71 let value: T72 let expiresAt: Date73}Offline-First Approach
swift
1// MARK: - Repository with Offline Support2class ProductRepository {3 private let networkService: NetworkServiceProtocol4 private let cacheManager: CacheManager5 private let reachability: NetworkReachability6 7 init(8 networkService: NetworkServiceProtocol = NetworkService.shared,9 cacheManager: CacheManager = .shared,10 reachability: NetworkReachability = .shared11 ) {12 self.networkService = networkService13 self.cacheManager = cacheManager14 self.reachability = reachability15 }16 17 func getProducts(page: Int = 1) async throws -> [Product] {18 let cacheKey = "products_page_\(page)"19 20 // Offline: return cached data21 if !reachability.isConnected {22 if let cached: [Product] = await cacheManager.get(cacheKey) {23 return cached24 }25 throw NetworkError.noConnection26 }27 28 // Online: fetch and cache29 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.products35 } catch {36 // Fallback to cache on error37 if let cached: [Product] = await cacheManager.get(cacheKey) {38 return cached39 }40 throw error41 }42 }43}Request Interceptors
Request'leri manipüle etmek için interceptor pattern:
swift
1// MARK: - Request Interceptor2protocol RequestInterceptor {3 func intercept(_ request: inout URLRequest) async throws4}5 6// Auth Token Interceptor7class AuthInterceptor: RequestInterceptor {8 func intercept(_ request: inout URLRequest) async throws {9 guard let token = await AuthManager.shared.getValidToken() else {10 throw NetworkError.unauthorized11 }12 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")13 }14}15 16// Logging Interceptor17class LoggingInterceptor: RequestInterceptor {18 func intercept(_ request: inout URLRequest) async throws {19 #if DEBUG20 print("🌐 [\(Date())] \(request.httpMethod ?? "?") \(request.url?.absoluteString ?? "?")")21 #endif22 }23}24 25// Interceptor Chain26class 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 Network2 3actor NetworkMonitor {4 static let shared = NetworkMonitor()5 private let monitor = NWPathMonitor()6 7 private(set) var isConnected = true8 private(set) var connectionType: ConnectionType = .unknown9 10 enum ConnectionType { case wifi, cellular, ethernet, unknown }11 12 func startMonitoring() {13 monitor.pathUpdateHandler = { [weak self] path in14 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 == .satisfied21 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
- Apple Developer - URLSession
- WWDC21 - Accelerate networking with HTTP/3
- Swift.org - Structured Concurrency
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

