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, büyük bir production iOS kod tabanını strict concurrency moduna geçirirken sık karşılaşılan 12 yaygın tuzak, çözümleri 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:
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 işinin büyük kısmı Sendable uyumluluğu üzerine geçer. Bir tipin Sendable olması demek, multi-threaded paylaşımda güvenli olması demek.
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
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:
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
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
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
Büyük ve eski bir kod tabanında yüzlerce closure-based API bulunabilir. Bunların async/await'e migration'ı bir-anda yapılamaz.
Bridge pattern:
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?
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.
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.
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'ları her async call'a küçük bir maliyet ekler. Migration öncesi ve sonrası kendi uygulamanda karşılaştırman gereken metrikler:
Metrik | Neden izlenmeli | Nasıl ölçülür |
|---|---|---|
Launch time | Ana thread'e eklenen actor hop'ları ilk ekranı geciktirebilir | Instruments App Launch şablonu + MetricKit |
Memory baseline | Ek task ve actor nesneleri bellek tabanını yükseltebilir | Xcode Memory Report + MetricKit |
CPU under load | Sık actor hop'u yoğun akışlarda CPU payını artırabilir | Instruments Time Profiler |
Build time | Complete mode'da tip denetimi ağırlaşır, derleme uzar | Xcode Build Timeline |
Build time genelde en hissedilen artış. Çözüm: incremental build cache ve modülerleştirme.
Runtime overhead çoğu mobil uygulamada ihmal edilebilir düzeyde kalır; yine de sıcak yollarda (hot path) Time Profiler ile doğrula.
Crash tarafında beklenen kazanç, compile-time'da kapatılan data race'lerin runtime'da artık crash üretmemesi; etkisini migration öncesi ve sonrası crash analytics karşılaştırmasıyla ölç.
10. Migration Checklist
Production bir uygulamada migration için önerilen 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)
actorveya@MainActor - Phase 4: ViewModels
@MainActormark - Phase 5: UIKit ViewControllers
@MainActormark (SwiftUI views zaten) - Phase 6:
SWIFT_STRICT_CONCURRENCY = completeaç - 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ü büyük bir kod tabanında migration haftalar, hatta aylar sürebilir; uzun ömürlü bir branch'te çatışma 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ü, bu sınıftaki runtime crash'leri kaynağında kapattığı için değerli. Ama migration ucuz değil — büyük bir kod tabanında aylara yayılabilen efor, belirgin şekilde uzayan build time ve küçük bir runtime overhead'i hesaba katılmalı.
Pragmatik karar: yeni projelerde Swift 6 + complete strict concurrency default. Mevcut projelerde targeted → complete geçişi aşamalı bir plan ile module-by-module. @MainActor her şeyin çaresi değil; doğru kullanım nonisolated ile heavy work'ü background'a tutmak.
İlgili kaynaklar:
Etiketler
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
