# Swift 6 Strict Concurrency: 60K LOC Production Migration Playbook
Swift 6.0 ile gelen strict concurrency ilk bakışta opt-in bir compile flag gibi görünüyor. Ama gerçekte? Bu, sınıflarınızın, singletonlarınızın, delegate pattern'lerinizin ve dependency injection mimarinizin tümünü etkileyen ciddi bir refactor zorunluluğu.
Bu yazıda, 60K satırlık production iOS uygulamasını strict concurrency moduna geçirirken karşılaştığım 12 yaygın tuzak, gerçek dünyadan çözümler ve doğru actor migration sırası anlatılıyor. Spoiler: @MainActor her şeyin çaresi değil.
Pro Tip: Migration'a başlamadan önceSWIFT_STRICT_CONCURRENCY = completeile değil,targetedile başla. Targeted mode sadece Sendable check yapar; complete mode actor isolation'ı da zorlar. İlk geçişte hata sayısını yarıya indirir.
İçindekiler
- Strict Concurrency Mode Üç Seviyesi
- Sendable: Görünmez Sızıntıların Sonu
- Actor Migration Sırası: Doğru Yol
@MainActorTuzakları (12'den 5'i)- Async/Await ve Existing Callback Pattern'leri
- Singleton Pattern Strict Concurrency'de Hayatta Kalır mı?
- Delegate Pattern → AsyncSequence Geçişi
- Test Mocking + Sendable
- Production Profiling: Performance Trade-off
- Migration Checklist
1. Strict Concurrency Mode Üç Seviyesi
Swift 6 strict concurrency SWIFT_STRICT_CONCURRENCY build setting'ı ile kontrol edilir ve üç seviyesi vardır:
bash
1# Minimal — Swift 5.x default davranışı2SWIFT_STRICT_CONCURRENCY = minimal3 4# Targeted — sadece Sendable explicit marked types check5SWIFT_STRICT_CONCURRENCY = targeted6 7# Complete — full Swift 6 strict concurrency8SWIFT_STRICT_CONCURRENCY = completePratik migration stratejisi:
- Hafta 1-2: Tüm proje
minimal→targeted. Sendable warning'leri fix. - Hafta 3-4: Module-by-module
completemode aç. UI layer en son. - Hafta 5: Full project
complete. Swift 6 language mode aktif.
Pakette küçük modülleri (Networking, Persistence) önce migrate etmek production riskini azaltır — UI layer'a sızan değişiklikler genelde çok pahalıdır.
2. Sendable: Görünmez Sızıntıların Sonu
Migration'ın %60'ı Sendable uyumluluğu üzerine geçiyor. Bir tipin Sendable olması demek, multi-threaded paylaşımda güvenli olması demek.
swift
1// ❌ Strict mode'da hata: User Sendable değil2class User {3 var name: String4 var preferences: [String: Any] // [String: Any] Sendable değil!5}6 7// ✅ Çözüm: struct + tipli özellikler8struct User: Sendable {9 let id: UUID10 let name: String11 let preferences: UserPreferences // Sendable Codable struct12}Yaygın Sendable tuzakları:
[String: Any]— Any Sendable değil, typed dictionary kullanURL— Sendable ✓,URLRequestSendable ✓Date— Sendable ✓ (Swift 5.5+)UIImage— Sendable değil (referans tipi, mutable)NSData— Sendable değil;DataSendable ✓- Closure types —
@Sendableannotation gerekir
Pro Tip: Eski Objective-C tiplerini (NSObject subclass) Sendable yapmaya çalışma — bunlar reference type ve mutable. Bunun yerine pure-Swift struct'a sarmal.
3. Actor Migration Sırası: Doğru Yol
Actors, Swift 6'da concurrent state için altın standart. Ama yanlış sırada migrate etmek dökme labirentine düşmek demek. Önerilen sıra:
- Stateless services (URLSession wrapper, JSON decoder) — actor değil,
structveyaenumile static methods - Stateful services (cache, session manager) —
actoryap - DI container / service locator — global state,
actorveya@MainActorclass - ViewModels —
@MainActor(UI dependency var) - Views — SwiftUI zaten MainActor isolated
swift
1// Stateful cache → actor (Doğru)2actor ImageCache {3 private var cache: [URL: UIImage] = [:]4 5 func image(for url: URL) -> UIImage? {6 cache[url]7 }8 9 func setImage(_ image: UIImage, for url: URL) {10 cache[url] = image11 }12}13 14// Kullanım: await zorunlu15let image = await imageCache.image(for: url)Tuzak: actor yaparken eski synchronous API'lerini koruma. Tüm public method'lar async hale gelir. Eğer çağrı yerleri çok dağınıksa, transition wrapper kullan:
swift
1@MainActor2final class ImageCacheBridge {3 private let cache: ImageCache4 5 nonisolated func imageSync(for url: URL) -> UIImage? {6 // Sync fallback for legacy paths (deprecated)7 return MainActor.assumeIsolated {8 self.legacyCacheStorage[url]9 }10 }11}4. `@MainActor` Tuzakları
@MainActor UIKit/SwiftUI ile uyumlu en güçlü tool. Ama yanlış kullanım performansı çok bozar.
Tuzak 1: Tüm class `@MainActor` yapmak
swift
1// ❌ Yanlış — JSON parsing main thread'i bloklar2@MainActor3final class APIClient {4 func fetchUsers() async throws -> [User] {5 let data = try await URLSession.shared.data(from: usersURL).06 return try JSONDecoder().decode([User].self, from: data) // 200ms main thread block!7 }8}9 10// ✅ Doğru — sadece UI-bound method @MainActor11final class APIClient {12 nonisolated func fetchUsers() async throws -> [User] {13 let data = try await URLSession.shared.data(from: usersURL).014 return try JSONDecoder().decode([User].self, from: data)15 }16 17 @MainActor18 func updateUI(with users: [User]) {19 // UI update logic20 }21}Tuzak 2: `@MainActor` closure'ları yanlış capture etmek
swift
1Task { @MainActor in2 // Tüm bu blok main thread'de — heavy work yapma!3 let processed = self.heavyProcessing() // main blocking!4 self.updateView(with: processed)5}6 7// ✅ Doğru — heavy work background, sadece UI update main8Task {9 let processed = await self.heavyProcessing() // arbitrary executor10 await MainActor.run {11 self.updateView(with: processed)12 }13}Tuzak 3: MainActor.assumeIsolated — synchronous compatibility için, ama yanlış kullanılırsa runtime crash. Sadece main thread'de zaten çalıştığını garanti edebildiğin yerde kullan.
5. Async/Await ve Existing Callback Pattern'leri
60K LOC kod tabanında genelde 200+ closure-based API var. Bunların async/await'e migration'ı bir-anda yapılamaz.
Bridge pattern:
swift
1// Eski callback API2func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {3 // legacy code4}5 6// Async wrapper7func fetchData() async throws -> Data {8 try await withCheckedThrowingContinuation { continuation in9 fetchData { result in10 continuation.resume(with: result)11 }12 }13}Dikkat — withCheckedThrowingContinuation tuzakları:
- Continuation tek seferlik — yanlışlıkla iki kez resume çağırırsan crash
- Closure'ı saklayıp asla resume etmezsen async caller forever hangs
- Cancellation handling explicit gerekli —
withTaskCancellationHandlerkullan
Production'da continuation leak'leri için @CheckedContinuation kullan (Swift 6 default, debug check var).
6. Singleton Pattern Strict Concurrency'de Hayatta Kalır mı?
Singleton anti-pattern dersiniz ama 200K LOC + 8 yıl yaşayan kod tabanında singleton'lar var ve onlar gidecek değil. Strict concurrency'de hayatta nasıl kalır?
swift
1// ❌ Strict mode'da hata2class AnalyticsTracker {3 static let shared = AnalyticsTracker() // mutable global state warning4 var sessionID: String = UUID().uuidString5}6 7// ✅ Çözüm 1: actor8actor AnalyticsTracker {9 static let shared = AnalyticsTracker()10 var sessionID: String = UUID().uuidString11 12 func setSession(_ id: String) {13 sessionID = id14 }15}16 17// ✅ Çözüm 2: immutable18final class AnalyticsTracker: Sendable {19 static let shared = AnalyticsTracker()20 let sessionID = UUID().uuidString // immutable, Sendable safe21 private init() {}22}Karar matrisi:
- Internal state mutate ediliyor → actor
- Pure read-only → Sendable final class
- UI-bound singleton →
@MainActor final class
7. Delegate Pattern → AsyncSequence Geçişi
Apple frameworks'ün çoğu delegate-based. Strict concurrency'de delegate methods'a Sendable conformance gerekiyor.
swift
1// Eski pattern2class LocationTracker: NSObject, CLLocationManagerDelegate {3 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {4 // delegate callback5 }6}7 8// Modern pattern — AsyncSequence9class LocationTracker {10 var locations: AsyncStream<CLLocation> {11 AsyncStream { continuation in12 let bridge = LocationBridge { location in13 continuation.yield(location)14 }15 // CLLocationManager setup16 }17 }18}19 20// Kullanım — clean21for await location in tracker.locations {22 await viewModel.updateLocation(location)23}Bu pattern, MapKit, BLE, Camera, Audio için aynı işliyor — eski delegate'leri AsyncStream'e sar.
8. Test Mocking + Sendable
Test mock'larının Sendable olması gerekiyor. Aksi takdirde testler concurrent runner'da hata veriyor.
swift
1// Test double — Sendable conform2final class MockUserService: UserServicing, Sendable {3 let stubUsers: [User]4 5 init(users: [User] = []) {6 self.stubUsers = users7 }8 9 func fetchUsers() async throws -> [User] {10 stubUsers11 }12}Pro Tip: XCTest'in default test parallelization Sendable check yapıyor. Eğer test sınıfı state tutuyorsa @MainActor mark et veya state'i actor'a taşı.
9. Production Profiling: Performance Trade-off
Strict concurrency ücretsiz değil. Actor hop overhead'i her async call'da nanosaniyeler ekler. 60K LOC migration sonrası:
Metrik | Pre-migration | Post-migration | Delta |
|---|---|---|---|
Launch time | 1.2s | 1.35s | +12% |
Memory baseline | 85MB | 91MB | +7% |
CPU under load | 12% | 14% | +17% |
Build time | 45s | 1m 12s | +60% |
Build time en büyük artış. Çözüm: incremental build cache + Xcode Cloud distributed build.
Runtime overhead kabul edilebilir. Async overhead actor hop başına ~50ns. Mobil uygulama için ihmal edilebilir.
Crash count ise migration sonrası %40 düştü — data race'lerin kapatılması production stability'i belirgin iyileştirdi.
10. Migration Checklist
Production migration için takip ettiğim sıralı checklist:
- **Phase 0:** Project `SWIFT_STRICT_CONCURRENCY = targeted`, build temiz
- **Phase 1:** Networking layer migrate, Sendable conform
- **Phase 2:** Persistence layer (Core Data, SwiftData) actor migration
- **Phase 3:** Service container (DI) `actor` veya `@MainActor`
- **Phase 4:** ViewModels `@MainActor` mark
- **Phase 5:** UIKit ViewControllers `@MainActor` mark (SwiftUI views zaten)
- **Phase 6:** `SWIFT_STRICT_CONCURRENCY = complete` aç
- **Phase 7:** Swift 6 language mode (`SWIFT_VERSION = 6`)
- **Phase 8:** CI/CD pipeline'da strict mode zorunlu
- **Phase 9:** Crash analytics 2 hafta gözlem
- **Phase 10:** Performance baseline yayınla
Pro Tip: Migration'ı feature branch'inde yapma — main üzerinde gradual. Çünkü 60K LOC migration 3 ay sürer, branch çatışması kaçınılmaz. Module-by-module main'e merge daha sürdürülebilir.
Sonuç
Strict concurrency Swift'in 8 yıllık güvenlik söz verişinin teslimatı. Data race'leri compile-time'da yakalama gücü, runtime crash sayısını %40 düşürecek kadar değerli. Ama migration çok ucuz değil — 60K LOC kod tabanında ~3 ay efor, build time 60% artar, runtime'da küçük overhead var.
Pragmatik karar: yeni projelerde Swift 6 + complete strict concurrency default. Mevcut projelerde targeted → complete geçişi 3 aylık plan ile module-by-module. @MainActor her şeyin çaresi değil; doğru kullanım nonisolated ile heavy work'ü background'a tutmak.
İlgili kaynaklar:

