Tüm Yazılar
KategoriSwift
Okuma Süresi
15 dk
Yayın Tarihi
...
Kelime Sayısı
1.590kelime

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

Strict concurrency mode opt-in flag'inden production-ready koda — 60K LOC üretim iOS uygulamasını migrate ederken karşılaştığım 12 tuzak, çözümler ve actor migration sırası.

Swift 6 Strict Concurrency: 60K LOC Production Migration Playbook

# 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 önce SWIFT_STRICT_CONCURRENCY = complete ile değil, targeted ile 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

  1. Strict Concurrency Mode Üç Seviyesi
  2. Sendable: Görünmez Sızıntıların Sonu
  3. Actor Migration Sırası: Doğru Yol
  4. @MainActor Tuzakları (12'den 5'i)
  5. Async/Await ve Existing Callback Pattern'leri
  6. Singleton Pattern Strict Concurrency'de Hayatta Kalır mı?
  7. Delegate Pattern → AsyncSequence Geçişi
  8. Test Mocking + Sendable
  9. Production Profiling: Performance Trade-off
  10. 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 = minimal
3 
4# Targeted — sadece Sendable explicit marked types check
5SWIFT_STRICT_CONCURRENCY = targeted
6 
7# Complete — full Swift 6 strict concurrency
8SWIFT_STRICT_CONCURRENCY = complete

Pratik migration stratejisi:

  1. Hafta 1-2: Tüm proje minimaltargeted. Sendable warning'leri fix.
  2. Hafta 3-4: Module-by-module complete mode aç. UI layer en son.
  3. 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ğil
2class User {
3 var name: String
4 var preferences: [String: Any] // [String: Any] Sendable değil!
5}
6 
7// ✅ Çözüm: struct + tipli özellikler
8struct User: Sendable {
9 let id: UUID
10 let name: String
11 let preferences: UserPreferences // Sendable Codable struct
12}

Yaygın Sendable tuzakları:

  • [String: Any] — Any Sendable değil, typed dictionary kullan
  • URL — Sendable ✓, URLRequest Sendable ✓
  • Date — Sendable ✓ (Swift 5.5+)
  • UIImage — Sendable değil (referans tipi, mutable)
  • NSData — Sendable değil; Data Sendable ✓
  • Closure types — @Sendable annotation 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:

  1. Stateless services (URLSession wrapper, JSON decoder) — actor değil, struct veya enum ile static methods
  2. Stateful services (cache, session manager) — actor yap
  3. DI container / service locator — global state, actor veya @MainActor class
  4. ViewModels@MainActor (UI dependency var)
  5. 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] = image
11 }
12}
13 
14// Kullanım: await zorunlu
15let 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@MainActor
2final class ImageCacheBridge {
3 private let cache: ImageCache
4 
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 bloklar
2@MainActor
3final class APIClient {
4 func fetchUsers() async throws -> [User] {
5 let data = try await URLSession.shared.data(from: usersURL).0
6 return try JSONDecoder().decode([User].self, from: data) // 200ms main thread block!
7 }
8}
9 
10// ✅ Doğru — sadece UI-bound method @MainActor
11final class APIClient {
12 nonisolated func fetchUsers() async throws -> [User] {
13 let data = try await URLSession.shared.data(from: usersURL).0
14 return try JSONDecoder().decode([User].self, from: data)
15 }
16 
17 @MainActor
18 func updateUI(with users: [User]) {
19 // UI update logic
20 }
21}

Tuzak 2: `@MainActor` closure'ları yanlış capture etmek

swift
1Task { @MainActor in
2 // 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 main
8Task {
9 let processed = await self.heavyProcessing() // arbitrary executor
10 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 API
2func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
3 // legacy code
4}
5 
6// Async wrapper
7func fetchData() async throws -> Data {
8 try await withCheckedThrowingContinuation { continuation in
9 fetchData { result in
10 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 — withTaskCancellationHandler kullan

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 hata
2class AnalyticsTracker {
3 static let shared = AnalyticsTracker() // mutable global state warning
4 var sessionID: String = UUID().uuidString
5}
6 
7// ✅ Çözüm 1: actor
8actor AnalyticsTracker {
9 static let shared = AnalyticsTracker()
10 var sessionID: String = UUID().uuidString
11 
12 func setSession(_ id: String) {
13 sessionID = id
14 }
15}
16 
17// ✅ Çözüm 2: immutable
18final class AnalyticsTracker: Sendable {
19 static let shared = AnalyticsTracker()
20 let sessionID = UUID().uuidString // immutable, Sendable safe
21 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 pattern
2class LocationTracker: NSObject, CLLocationManagerDelegate {
3 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
4 // delegate callback
5 }
6}
7 
8// Modern pattern — AsyncSequence
9class LocationTracker {
10 var locations: AsyncStream<CLLocation> {
11 AsyncStream { continuation in
12 let bridge = LocationBridge { location in
13 continuation.yield(location)
14 }
15 // CLLocationManager setup
16 }
17 }
18}
19 
20// Kullanım — clean
21for 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 conform
2final class MockUserService: UserServicing, Sendable {
3 let stubUsers: [User]
4 
5 init(users: [User] = []) {
6 self.stubUsers = users
7 }
8 
9 func fetchUsers() async throws -> [User] {
10 stubUsers
11 }
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:

Etiketler

#Swift 6#Concurrency#Migration#iOS#Actor#Sendable#Production
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ş

İlgili İçerik