Tüm Yazılar
KategoriSwift
Okuma Süresi
15 dk
Yayın Tarihi
2026-05-13
Kelime Sayısı
1.648kelime

Kahveni hazırla - bu içerikli bir makale!3 görüntülenme

Swift 6 Strict Concurrency: Production Migration Playbook

Özet

Strict concurrency mode opt-in flag'inden production-ready koda — büyük bir iOS kod tabanını migrate ederken sık karşılaşılan 12 tuzak, çözümler ve actor migration sırası.

  • Büyük kod tabanlarında migration aylara yayılabilir; build time, launch time ve memory'yi migration öncesi ve sonrası kendi uygulamanda ölç.
  • Actor migration sırası: stateless servisler, stateful servisler, DI container, ViewModel'ler, View'lar.
  • withCheckedThrowingContinuation'da tek seferlik resume kuralı ihlal edilirse crash veya sonsuz bekleme oluşur.
  • Tüm class'ı değil sadece UI-bound method'u @MainActor yap; crash etkisini migration öncesi ve sonrası crash analytics ile karşılaştır.
Swift 6 Strict Concurrency: 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, 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 ö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 minimal → targeted. 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 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.

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

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:

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'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) 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ü 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

#Swift 6#Concurrency#Migration#iOS#Actor#Sendable#Production
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

12+ yıllık deneyime sahip Senior Mobile Engineer. iOS, Flutter ve React Native ile mobil ürünler geliştiriyorum; full-stack ve AI-native çalışıyorum.

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

Paylaş

İlgili İçerik