• 17.08.2026 09:06:43
  • Admin Admin

iPhone uygulama geliştirme sürecinde URLSession, HTTP önbelleği, idempotent retry ve arka plan yüklemelerini birlikte tasarlayın. iOS MVVM mimarisi için test edilebilir bir ağ katmanı kurulumu.

Swift ile iOS Ağ Katmanı: HTTP Cache, Retry ve Offline Kuyruk

iOS MVVM mimarisi içinde test edilebilir ağ istemcisi

Bir ios eğitimi ya da swift eğitimi içeriğinde ağ çağrısını doğrudan ViewModel içine yazmak hızlı görünür; fakat timeout, HTTP hata eşleme ve test doubles davranışları ekranlara dağılır. iOS MVVM mimarisi için ViewModel'in yalnızca bir protokole bağımlı olması daha güvenlidir: URLSession'ın somut örneğini composition root'ta enjekte edin. Aşağıdaki actor, aynı istemcinin paylaşılan mutable durum taşımamasını ve her yanıtta HTTP durum kodunun gövde decode edilmeden denetlenmesini sağlar.

protocol APITransport: Sendable {
    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse)
}

enum APIError: Error {
    case invalidResponse
    case status(Int, Data)
}

actor URLSessionTransport: APITransport {
    private let session: URLSession

    init(session: URLSession) {
        self.session = session
    }

    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        guard (200...299).contains(http.statusCode) else {
            throw APIError.status(http.statusCode, data)
        }
        return (data, http)
    }
}

struct ProfileViewModel {
    let transport: any APITransport

    func loadProfile() async throws -> Profile {
        var request = URLRequest(url: URL(string: "https://api.example.com/me")!)
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        let (data, _) = try await transport.send(request)
        return try JSONDecoder().decode(Profile.self, from: data)
    }
}

XCTest'te URLProtocol tabanlı stub yerine bu protokolü kullanarak deterministik hata senaryoları yazın: örneğin 429, 503 ve bozuk JSON'u ayrı test doubles ile üretin. İncelik şudur: `URLSession.data(for:)` 404 veya 500 için otomatik hata fırlatmaz; yalnızca taşıma hatalarında throw eder. Bu nedenle HTTP status denetimini transport katmanında atlamak, ViewModel'in başarısız bir hata gövdesini `Profile` olarak decode etmeye çalışmasına yol açar. Bu ayrım, bir swift kursu laboratuvarında da unit test sınırını görünür kılar.

URLCache ile HTTP cache sözleşmesini doğru kurmak

HTTP cache, istemcide tek başına açılan bir anahtar değildir; sunucunun `Cache-Control`, `ETag`, `Last-Modified` ve gerektiğinde `Vary` başlıklarıyla yaptığı sözleşmeye bağlıdır. Uygulama başlangıcında ayrı bir `URLCache` kurun ve yalnızca GET isteklerinde `.useProtocolCachePolicy` kullanın. Sunucu `Cache-Control: no-store` döndürüyorsa URLSession'ın bu yanıtı diske yazmaması beklenen davranıştır; bunu istemci tarafında zorla aşmak kimlik veya hesap verisi sızıntısı üretir.

let cache = URLCache(
    memoryCapacity: 20 * 1024 * 1024,
    diskCapacity: 100 * 1024 * 1024,
    diskPath: "api-http-cache"
)

let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
configuration.requestCachePolicy = .useProtocolCachePolicy
configuration.timeoutIntervalForRequest = 20

let apiSession = URLSession(configuration: configuration)

var request = URLRequest(url: URL(string: "https://api.example.com/catalog")!)
request.httpMethod = "GET"
request.cachePolicy = .useProtocolCachePolicy

Xcode'daki Instruments Network şablonunda aynı katalog ekranını önce boş cache ile, sonra ikinci açılışta çalıştırın; karşılaştırmada işlem sayısı, indirilen byte ve yanıtın `resourceFetchType` değeri kaydedilmelidir. Sunucu `ETag` döndürüyorsa ikinci istekte koşullu doğrulama ile 304 görülmesi beklenir; 304 bir hata değil, gövdenin mevcut cache kaydından alınması gerektiğinin işaretidir. Kimlik doğrulamalı yanıtlar için özellikle `Vary: Authorization` veya kullanıcıya özel `Cache-Control: private` doğrulanmalıdır. Aksi halde ortak cache anahtarında A kullanıcısının verisini B kullanıcısına verme riski vardır. Bu ölçümü release benzeri bir scheme'de yapın; Debug'da proxy, loglama veya lokal backend davranışı sonucu yanıltabilir.

Offline outbox: idempotency anahtarı ve geri çekilmeli retry

Bağlantı yokken oluşturulan sipariş, not veya form verisini yalnızca `NWPathMonitor` ile yeniden göndermeyin. Monitor, bir yolun var olduğunu bildirir; DNS çözümlemesinin, captive portalın veya API'nin sağlıklı olduğunu kanıtlamaz. Gönderilecek komutu SwiftData ile kalıcı bir outbox'a yazın, idempotency anahtarını istekten önce üretin ve sunucuda `(userId, idempotencyKey)` için unique constraint uygulayın. Böylece istemci timeout sonrası aynı POST'u tekrar gönderse bile sunucu ikinci kaydı üretmez.

@Model
final class PendingCommand {
    @Attribute(.unique) var idempotencyKey: String
    var endpoint: String
    var body: Data
    var attempts: Int
    var nextAttemptAt: Date

    init(endpoint: String, body: Data) {
        self.idempotencyKey = UUID().uuidString
        self.endpoint = endpoint
        self.body = body
        self.attempts = 0
        self.nextAttemptAt = .now
    }
}

func makeRequest(from command: PendingCommand) -> URLRequest {
    var request = URLRequest(url: URL(string: command.endpoint)!)
    request.httpMethod = "POST"
    request.httpBody = command.body
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue(command.idempotencyKey, forHTTPHeaderField: "Idempotency-Key")
    return request
}

func fullJitterDelay(attempt: Int) -> TimeInterval {
    let capped = min(60.0, pow(2.0, Double(attempt)))
    return Double.random(in: 0...capped)
}

Retry kuyruğunda sadece `URLError.notConnectedToInternet`, `URLError.timedOut`, 408, 429 ve 5xx gibi geçici sınıfları yeniden deneyin; 400, 401, 403 ve çoğu 422 yanıtını kullanıcı müdahalesi veya kalıcı hata olarak işaretleyin. 429 için gövdeden süre hesaplamak yerine önce HTTP `Retry-After` başlığını parse edin; başlık yoksa full-jitter backoff kullanın. Başarılı yanıt, aynı idempotency anahtarıyla dönmüş önceki işlemin sonucu da olabilir; API sözleşmesi bu durumu 200/201 veya açık bir durum alanıyla tanımlamalıdır. Bu desen, iphone uygulama geliştirme sırasında uygulamanın öldürülmesi ile ağ timeout'unun aynı kullanıcı işlemini iki kez kaydetmesini engeller.

Arka plan dosya yüklemeleri ve app store yayınlama öncesi doğrulama

Büyük medya dosyalarında `dataTask` ile belleğe `Data` yüklemek yerine background `URLSession` ve `uploadTask(with:fromFile:)` kullanın. Background oturum, uygulama askıya alındığında transferi sistem daemon'ına devreder; fakat request body için dosya URL'si kullanmak gerekir. Bellekteki `Data` ile başlatılan yüklemeye güvenmek, uygulama sonlandırılırsa yeniden bağlanabilirlik beklentisini bozar.

final class UploadDelegate: NSObject, URLSessionTaskDelegate, URLSessionDelegate {
    var backgroundCompletion: (() -> Void)?

    lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.example.product.media-upload"
        )
        config.sessionSendsLaunchEvents = true
        config.isDiscretionary = false
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    func enqueue(fileURL: URL, uploadURL: URL) {
        var request = URLRequest(url: uploadURL)
        request.httpMethod = "PUT"
        request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
        session.uploadTask(with: request, fromFile: fileURL).resume()
    }

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            self.backgroundCompletion?()
            self.backgroundCompletion = nil
        }
    }
}

Uygulama delegate'inde `application(_:handleEventsForBackgroundURLSession:completionHandler:)` callback'inden gelen completion handler'ı saklayıp yalnızca `urlSessionDidFinishEvents` içinde çağırın. App switcher'dan uygulamayı kapatıp, cihazı kilitleyip ve ağ tipini değiştirip gerçek cihazda test edin; simulator bu yaşam döngüsünü yeterince temsil etmez. app store yayınlama öncesinde Privacy Manifest içindeki kullanılan API gerekçelerini ve arka plan modlarını da Xcode target ayarları ile eşleştirin; dosya transferi için gereksiz `UIBackgroundModes` eklemek, incelemede açıklanması zor bir yetki yüzeyi oluşturur.

URLSessionTaskMetrics ile cache ve ağ davranışını ölçmek

Cache veya retry davranışını yalnızca log satırlarından değerlendirmeyin. `URLSessionTaskMetrics`, her task için DNS, TCP, TLS, request ve response zamanlarını; ayrıca `resourceFetchType` ile local cache, network load veya server push kaynağını verir. Delegate'te bu değerleri anonimleştirilmiş endpoint sınıfı ve HTTP durum koduyla birleştirip OSLog'a yazın; ham URL'de kullanıcı kimliği veya erişim token'ı taşımayın.

final class MetricsDelegate: NSObject, URLSessionTaskDelegate {
    private let logger = Logger(subsystem: "com.example.product", category: "network")

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didFinishCollecting metrics: URLSessionTaskMetrics
    ) {
        guard let tx = metrics.transactionMetrics.last else { return }
        let dns = tx.domainLookupEndDate?.timeIntervalSince(tx.domainLookupStartDate ?? .now) ?? 0
        let tls = tx.secureConnectionEndDate?.timeIntervalSince(tx.secureConnectionStartDate ?? .now) ?? 0
        logger.info("fetch=\(String(describing: tx.resourceFetchType), privacy: .public) dns=\(dns, format: .fixed(precision: 3)) tls=\(tls, format: .fixed(precision: 3))")
    }
}

Değişiklikten önce ve sonra aynı test hesabıyla 30 soğuk, 30 sıcak katalog açılışı yapın; p50/p95 toplam süreyi, transfer byte'ını ve `resourceFetchType == .localCache` oranını karşılaştırın. Bu yaklaşım bir swiftui eğitimi içinde ekranın `task` modifier'ını tartışırken de kritiktir: görünüm yeniden yaratıldığında aynı GET'in kaç kez tetiklendiğini Instruments Network ile sayabilirsiniz. xcode eğitimi kapsamında Scheme Diagnostics'teki Network Link Conditioner ile %3 packet loss ve yüksek gecikme profili altında outbox'ın duplicate kayıt üretmediğini doğrulamak, yalnızca Wi-Fi'da yapılan mutlu yol testinden daha değerlidir.

Sık Sorulan Sorular

iOS MVVM mimarisi içinde URLSession ViewModel'e mi enjekte edilmeli?

ViewModel'e URLSession yerine `APITransport` gibi küçük bir protokol enjekte edin. Üretimde `URLSessionTransport`, testte ise belirli `Data` veya `APIError.status(429, ...)` döndüren fake kullanın. Böylece ViewModel testinde URLProtocol global kayıt sırası ve URLCache yan etkileri oluşmaz.

Swift eğitimi için URLCache kullanırken ETag'i istemci ayrıca saklamalı mı?

Sunucu doğru `Cache-Control` ve `ETag` başlıklarını döndürüyor, istek `.useProtocolCachePolicy` kullanıyorsa URLSession normal HTTP doğrulamasını yürütebilir. Özel ETag saklama ancak GraphQL POST, uygulamaya özgü stale-while-revalidate veya cache anahtarı için kullanıcı/tenant ayrımı gibi HTTP cache'in kapsamadığı bir gereksinimde anlamlıdır.

iPhone uygulama geliştirme sırasında offline POST retry neden duplicate kayıt oluşturur?

İstemci timeout aldığında sunucunun işlemi tamamlayıp tamamlamadığını bilemez; aynı POST'u yeni bir kimlikle tekrar gönderirse sunucu bunu yeni komut sayar. Kalıcı `Idempotency-Key`, sunucuda unique constraint ve aynı anahtar için önceki sonucu döndürme kuralı bu belirsizliği çözer.

iOS kursu projelerinde background URLSession upload için Data mı dosya URL'si mi kullanılmalı?

Uygulama askıya alınsa veya sonlandırılsa da devam etmesi gereken yüklemelerde `uploadTask(with:fromFile:)` kullanın. Geçici dosyayı transfer tamamlanana kadar silmeyin; `urlSession(_:task:didCompleteWithError:)` callback'inde başarı veya kalıcı hata sonucuna göre temizleyin.

AI / LLM Discovery

Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt