Main actor, @concurrent ve [weak self]: Swift 6.2 için concurrency alışkanlıkları
Swift 6.2'deki varsayılan main actor izolasyonu, nonisolated(nonsending) ve @concurrent kodun nerede çalıştığını nasıl değiştiriyor, async neden arka plan demek değil ve Task içinde [weak self] ne zaman hâlâ önemli.
GCD döneminden kalma iki alışkanlık Swift kodunda hâlâ her yerde: her UI güncellemesini DispatchQueue.main.async içine sarmak ve her closure'a [weak self] eklemek. Swift concurrency ile ikisi de çoğu zaman gereksiz, bazen de gerçek hataları gizliyor. async'in "arka plan" demek olduğu varsayıldığı için bir Task arayüzü dondurabiliyor. Uzun ömürlü bir döngü bir view controller'ı sonsuza kadar hayatta tutabiliyor.
Swift 6.2 (15 Eylül 2025'te yayınlandı) varsayılanları ikisini de yeniden düşünmeye değecek kadar değiştirdi. Swift 6.4 Eylül 2026'da çıktı, ama bu yazıdaki model 6.2 ile gelen model. Aşağıda kodun gerçekte nerede çalıştığını, 6.2'nin neyi değiştirdiğini, arayüz kodunu bugün nasıl yazmanızı önerdiğimi ve weak capture'ın ne zaman hâlâ gerekli olduğunu anlatıyorum.
Thread'lerden actor'lere
GCD döneminde kurallar basitti ve tamamen elle uygulanıyordu. UIKit'e main thread'de dokunmak gerekiyordu, dolayısıyla her completion handler DispatchQueue.main.async { self.label.text = ... } ile bitiyordu. Escaping closure'lar retain cycle yaratabiliyordu, dolayısıyla [weak self] bir reflekse dönüştü, ardından da her yerde guard let self ya da self?. geldi.
Swift concurrency (iOS 13'e kadar geriye kullanılabiliyor) "hangi thread" sorusunu "hangi actor" sorusuyla değiştirdi. @MainActor kod main thread'de çalışıyor ve derleyici izolasyon alanları arasındaki her geçişi kontrol ediyor. Swift 6 dil modu da birçok data race hatasını derleme hatasına çeviriyor. Eski snippet'ler bu dünyada da derleniyor. Sadece gürültülüler, bazen de ince bir şekilde yanlışlar.
async, arka plan demek değil
En yaygın yanlış anlama birkaç satıra sığıyor. CSV export hazırlayan bir view model düşünün:
final class LegacyExportModel: ObservableObject {
@Published var csv = ""
func buildCSV(rows: [Row]) async {
var output = "id,name,amount\n"
for row in rows { // büyük bir export'ta saniyelerce CPU işi
output += "\(row.id),\(row.name),\(row.amount)\n"
}
csv = output
}
}
// Bir SwiftUI buton aksiyonunda:
Task { await model.buildCSV(rows: rows) }
async olduğu için buildCSV'nin arka planda çalışacağını düşünmek kolay. Çalışıp çalışmadığı iki kurala bağlı:
Task { }, onu oluşturan bağlamın actor'ünü miras alıyor. SwiftUI view'undaki bir buton aksiyonu main actor'de çalışıyor, dolayısıyla task da orada başlıyor.- İzole olmayan bir
asyncfonksiyonun nerede çalıştığı zaman içinde değişti. Swift 5.7'den önce bu net tanımlı değildi. SE-0338 o dönemin implementasyonunu, bu fonksiyonların "never intentionally change the current executor" diye anlatıyor. Yani böyle bir kod main actor'de çalışmaya devam edip arayüzü donduruyordu.
SE-0338 (Swift 5.7) kuralı netleştirdi: izole olmayan async fonksiyonlar actor'den çıkıp global concurrent executor'a geçiyor. Bu, bu donmayı önledi ama kodu anlamayı zorlaştırdı. İzole olmayan bir async fonksiyona yapılan her çağrı bir izolasyon sınırını geçiyordu ve Swift 6'da karşılaşılan Sendable hatalarının birçoğu tam da buradan geliyor.
Swift 6.2 neyi değiştiriyor
Swift 6.2 isteğe bağlı iki değişiklik getirdi. İkisi de kabul edilmiş ve implemente edilmiş proposal'lar.
SE-0461, nonisolated(nonsending) ve @concurrent. NonisolatedNonsendingByDefault upcoming feature'ı açıkken izole olmayan bir async fonksiyon varsayılan olarak çağıranın actor'ünde çalışıyor. Bunu açıkça nonisolated(nonsending) diye de yazabilirsiniz. Gerçekten actor'den çıkmak istediğinizde fonksiyonu @concurrent ile işaretliyorsunuz. Bu özellik açıkken yukarıdaki buildCSV yine main actor'de çalışır, ama bu sefer bu belgelenmiş davranış. Çözüm, ne istediğinizi açıkça söylemek:
nonisolated struct Row: Sendable {
let id: Int
let name: String
let amount: Decimal
}
@concurrent
func makeCSV(from rows: [Row]) async -> String {
var output = "id,name,amount\n"
for row in rows {
output += "\(row.id),\(row.name),\(row.amount)\n"
}
return output
}
@MainActor
@Observable
final class ExportModel {
private(set) var csv = ""
func export(_ rows: [Row]) async {
csv = await makeCSV(from: rows) // arayüz akıcı kalır, sonuç main actor'e döner
}
}
SE-0466, varsayılan actor izolasyonu. Bir modül, -default-isolation MainActor compiler flag'iyle işaretlenmemiş bütün kod için @MainActor çıkarımına geçebiliyor. Swift 6.2 duyurusu bunu script'ler, UI kodu ve diğer executable target'lar için uygun bir seçenek olarak sunuyor. Xcode'da build setting'in adı Default Actor Isolation (SWIFT_DEFAULT_ACTOR_ISOLATION). Ayrı bir Approachable Concurrency ayarı (SWIFT_APPROACHABLE_CONCURRENCY) da NonisolatedNonsendingByDefault'u içeren bir grup upcoming feature'ı açıyor. SwiftPM'de:
// swift-tools-version: 6.2
.target(
name: "AppFeature",
swiftSettings: [
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
]
)
Önerim: uygulama target'ında ve UI feature modüllerinde ikisini de açın. Networking, persistence ve diğer kütüphane modüllerini varsayılan olarak nonisolated bırakın ve sınırı neyin geçtiğine bilinçli karar verin.
DispatchQueue.main olmadan arayüz kodu
Bugün önerdiğim tarzda yazılmış bir feed ekranı aşağıda. Önemli kısımlar açıkça işaretlendiği için varsayılan main actor izolasyonu açık da olsa kapalı da olsa çalışıyor:
import SwiftUI
import Observation
nonisolated struct Post: Decodable, Sendable, Identifiable {
let id: Int
let title: String
}
nonisolated struct FeedAPI: Sendable {
func fetchData() async throws -> Data {
let url = URL(string: "https://example.com/feed.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
@concurrent
func decodePosts(from data: Data) async throws -> [Post] {
try JSONDecoder().decode([Post].self, from: data) // main actor dışında
}
@MainActor
@Observable
final class FeedModel {
private(set) var posts: [Post] = []
private(set) var errorMessage: String?
private let api = FeedAPI()
func refresh() async {
do {
let data = try await api.fetchData()
posts = try await decodePosts(from: data) // yeniden main actor'de, dispatch yok
} catch {
errorMessage = error.localizedDescription
}
}
}
struct FeedView: View {
@State private var model = FeedModel()
var body: some View {
List(model.posts) { Text($0.title) }
.task { await model.refresh() } // iOS 15+
.refreshable { await model.refresh() } // iOS 15+
}
}
Ne DispatchQueue.main var ne de MainActor.run. Model @MainActor olduğu için posts'a atama yapmak tasarım gereği güvenli. Bir tipte nonisolated kullanmak (SE-0449, Swift 6.1) onu modülün varsayılan main actor izolasyonunun dışında tutuyor, böylece main actor dışında rahatça kullanılabiliyor.
Callback köprüleri: MainActor.run mı, @MainActor closure mı
Bazen yine de bir köprüye ihtiyacınız oluyor, genelde kontrolünüzde olmayan completion handler'lı bir API'den gelirken:
- Senkron bir callback içinde
Task { @MainActor in ... }kullanın. - İzole olmayan bir async fonksiyonda
await MainActor.run { ... }(iOS 13+) çalışır. Ama genelde daha temiz çözüm, çağırdığınız fonksiyonu@MainActoryapıp doğrudanawaitetmek. - Bir callback'in main thread'de geldiği belgelenmişse,
MainActor.assumeIsolated { ... }(iOS 13+) bunu derleyiciye söylüyor. Varsayım yanlışsa uygulamayı fatal error ile durduruyor.
[weak self] ne zaman hâlâ önemli
Bir Task, closure'ını ve closure'ın yakaladığı her şeyi task bitene kadar tutuyor. Sonra bırakıyor. Bir saniyede biten bir istek için self'i strong yakalamak yalnızca nesnenin biraz daha uzun yaşaması demek. Bu bir leak değil, yani oradaki [weak self] çoğunlukla gürültü.
Task kendi kendine hiç bitmediğinde gerçek bir soruna dönüşüyor:
final class TickerViewController: UIViewController {
private let service = PriceService()
private let priceLabel = UILabel()
private var tickerTask: Task<Void, Never>?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let stream = service.prices() // AsyncStream<Price>
tickerTask = Task { [weak self] in
for await price in stream {
guard let self else { return } // sadece bu iterasyon boyunca strong
self.priceLabel.text = price.value.formatted()
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
tickerTask?.cancel() // döngüyü bitirir
tickerTask = nil
}
}
Task'ın sahibi view controller. Sonsuz bir döngü self'i strong yakalasaydı onu sonsuza kadar hayatta tutardı. İki detay önemli. guard let self döngünün içinde, yani self yalnızca bir eleman işlenirken tutuluyor. Task da açıkça iptal ediliyor. SwiftUI'da .task { for await ... } tercih edin: view kaybolduğunda otomatik iptal ediliyor.
Tuzaklar
- Task'ın en başında
guard let self.Task { [weak self] in guard let self else { return }; for await ... }yazmakself'i bütün döngü boyunca strong yapıyor, weak capture'ın hiçbir etkisi kalmıyor. - Sadece içteki closure'da weak.
loader.load { result in Task { @MainActor [weak self] in ... } }yazınca dıştaki closureself'i yine strong yakalıyor. Yeni derleyiciler tam olarak bu durum için uyarı veriyor, örneğin Swift 6.4.[weak self]'i en dıştaki escaping closure'a koyun. async'i arka plan sanmak.NonisolatedNonsendingByDefaultaçıkken açıkça öyle değil. CPU'yu yoran iş için@concurrentkullanın. Ben bunuTask.detached'a tercih ediyorum. Detached task, iptalini sizin yönetmeniz gereken unstructured bir task oluşturuyor ve Apple'ın dokümantasyonu da structured concurrency işi modelleyebiliyorsa detached task'tan kaçınmayı öneriyor.@MainActorkod içindeDispatchQueue.main.async. Zaten main actor'desiniz. Dispatch işi sadece run loop'un sonraki turuna erteliyor ve actor bilgisini derleyiciden saklıyor.- Varsayılan main actor izolasyonunu her yerde açmak. Uygulama ve UI kodu için tasarlandı. Bir networking paketinde, hiç main actor'e ait olmayan işleri main actor'e itiyor.
Kontrol listesi
- UI tipleri
@MainActor. Uygulama target'larındaDefault Actor Isolation = MainActorayarını düşünün (Swift 6.2+). NonisolatedNonsendingByDefault'u (ya da Xcode'daki Approachable Concurrency ayarını) açın ve CPU'yu yoran fonksiyonları@concurrentile işaretleyin.DispatchQueue.main.asyncyerine main actor metotlarınıawaitile çağırın.Task { @MainActor in }'i yalnızca callback sınırlarında kullanın.- Kısa task'larda refleks
[weak self]'i bırakın. Uzun ömürlü döngülerde iptalle birlikte koruyun. - SwiftUI'da elle saklanan task'lar yerine
.task {}ve.task(id:)(iOS 15+) kullanın.
GCD dönemindeki kurallar kendi zamanları için doğruydu. Swift 6.2 ile bu kuralların çoğunu derleyici sizin yerinize uygulayabiliyor, yeter ki kodunuzun nerede çalışması gerektiğini ona söyleyin.
- ---
- baslik: "Main actor, @concurrent ve [weak self]: Swift 6.2 için concurrency alışkanlıkları"
- ozet: "Swift 6.2'deki varsayılan main actor izolasyonu, nonisolated(nonsending) ve @concurrent kodun nerede çalıştığını nasıl değiştiriyor, async neden arka plan demek değil ve Task içinde [weak self] ne zaman hâlâ önemli."
- tarih: 2026-09-09
- etiketler:
- - ios
- - swift
- - concurrency
- kategoriler:
- - ios
- ---
- GCD döneminden kalma iki alışkanlık Swift kodunda hâlâ her yerde: her UI güncellemesini `DispatchQueue.main.async` içine sarmak ve her closure'a `[weak self]` eklemek. Swift concurrency ile ikisi de çoğu zaman gereksiz, bazen de gerçek hataları gizliyor. `async`'in "arka plan" demek olduğu varsayıldığı için bir `Task` arayüzü dondurabiliyor. Uzun ömürlü bir döngü bir view controller'ı sonsuza kadar hayatta tutabiliyor.
- Swift 6.2 (15 Eylül 2025'te yayınlandı) varsayılanları ikisini de yeniden düşünmeye değecek kadar değiştirdi. Swift 6.4 Eylül 2026'da çıktı, ama bu yazıdaki model 6.2 ile gelen model. Aşağıda kodun gerçekte nerede çalıştığını, 6.2'nin neyi değiştirdiğini, arayüz kodunu bugün nasıl yazmanızı önerdiğimi ve weak capture'ın ne zaman hâlâ gerekli olduğunu anlatıyorum.
- ## Thread'lerden actor'lere
- GCD döneminde kurallar basitti ve tamamen elle uygulanıyordu. UIKit'e main thread'de dokunmak gerekiyordu, dolayısıyla her completion handler `DispatchQueue.main.async { self.label.text = ... }` ile bitiyordu. Escaping closure'lar retain cycle yaratabiliyordu, dolayısıyla `[weak self]` bir reflekse dönüştü, ardından da her yerde `guard let self` ya da `self?.` geldi.
- Swift concurrency (iOS 13'e kadar geriye kullanılabiliyor) "hangi thread" sorusunu "hangi actor" sorusuyla değiştirdi. `@MainActor` kod main thread'de çalışıyor ve derleyici izolasyon alanları arasındaki her geçişi kontrol ediyor. Swift 6 dil modu da birçok data race hatasını derleme hatasına çeviriyor. Eski snippet'ler bu dünyada da derleniyor. Sadece gürültülüler, bazen de ince bir şekilde yanlışlar.
- ## async, arka plan demek değil
- En yaygın yanlış anlama birkaç satıra sığıyor. CSV export hazırlayan bir view model düşünün:
- ```swift
- final class LegacyExportModel: ObservableObject {
- @Published var csv = ""
- func buildCSV(rows: [Row]) async {
- var output = "id,name,amount\n"
- for row in rows { // büyük bir export'ta saniyelerce CPU işi
- output += "\(row.id),\(row.name),\(row.amount)\n"
- }
- csv = output
- }
- }
- // Bir SwiftUI buton aksiyonunda:
- Task { await model.buildCSV(rows: rows) }
- ```
- `async` olduğu için `buildCSV`'nin arka planda çalışacağını düşünmek kolay. Çalışıp çalışmadığı iki kurala bağlı:
- 1. `Task { }`, onu oluşturan bağlamın actor'ünü miras alıyor. SwiftUI view'undaki bir buton aksiyonu main actor'de çalışıyor, dolayısıyla task da orada başlıyor.
- 2. İzole olmayan bir `async` fonksiyonun nerede çalıştığı zaman içinde değişti. Swift 5.7'den önce bu net tanımlı değildi. SE-0338 o dönemin implementasyonunu, bu fonksiyonların "never intentionally change the current executor" diye anlatıyor. Yani böyle bir kod main actor'de çalışmaya devam edip arayüzü donduruyordu.
- SE-0338 (Swift 5.7) kuralı netleştirdi: izole olmayan async fonksiyonlar actor'den çıkıp global concurrent executor'a geçiyor. Bu, bu donmayı önledi ama kodu anlamayı zorlaştırdı. İzole olmayan bir async fonksiyona yapılan her çağrı bir izolasyon sınırını geçiyordu ve Swift 6'da karşılaşılan `Sendable` hatalarının birçoğu tam da buradan geliyor.
- ## Swift 6.2 neyi değiştiriyor
- Swift 6.2 isteğe bağlı iki değişiklik getirdi. İkisi de kabul edilmiş ve implemente edilmiş proposal'lar.
- **SE-0461, `nonisolated(nonsending)` ve `@concurrent`.** `NonisolatedNonsendingByDefault` upcoming feature'ı açıkken izole olmayan bir async fonksiyon varsayılan olarak *çağıranın* actor'ünde çalışıyor. Bunu açıkça `nonisolated(nonsending)` diye de yazabilirsiniz. Gerçekten actor'den çıkmak istediğinizde fonksiyonu `@concurrent` ile işaretliyorsunuz. Bu özellik açıkken yukarıdaki `buildCSV` yine main actor'de çalışır, ama bu sefer bu belgelenmiş davranış. Çözüm, ne istediğinizi açıkça söylemek:
- ```swift
- nonisolated struct Row: Sendable {
- let id: Int
- let name: String
- let amount: Decimal
- }
- @concurrent
- func makeCSV(from rows: [Row]) async -> String {
- var output = "id,name,amount\n"
- for row in rows {
- output += "\(row.id),\(row.name),\(row.amount)\n"
- }
- return output
- }
- @MainActor
- @Observable
- final class ExportModel {
- private(set) var csv = ""
- func export(_ rows: [Row]) async {
- csv = await makeCSV(from: rows) // arayüz akıcı kalır, sonuç main actor'e döner
- }
- }
- ```
- **SE-0466, varsayılan actor izolasyonu.** Bir modül, `-default-isolation MainActor` compiler flag'iyle işaretlenmemiş bütün kod için `@MainActor` çıkarımına geçebiliyor. Swift 6.2 duyurusu bunu script'ler, UI kodu ve diğer executable target'lar için uygun bir seçenek olarak sunuyor. Xcode'da build setting'in adı *Default Actor Isolation* (`SWIFT_DEFAULT_ACTOR_ISOLATION`). Ayrı bir *Approachable Concurrency* ayarı (`SWIFT_APPROACHABLE_CONCURRENCY`) da `NonisolatedNonsendingByDefault`'u içeren bir grup upcoming feature'ı açıyor. SwiftPM'de:
- ```swift
- // swift-tools-version: 6.2
- .target(
- name: "AppFeature",
- swiftSettings: [
- .defaultIsolation(MainActor.self),
- .enableUpcomingFeature("NonisolatedNonsendingByDefault")
- ]
- )
- ```
- Önerim: uygulama target'ında ve UI feature modüllerinde ikisini de açın. Networking, persistence ve diğer kütüphane modüllerini varsayılan olarak `nonisolated` bırakın ve sınırı neyin geçtiğine bilinçli karar verin.
- ## DispatchQueue.main olmadan arayüz kodu
- Bugün önerdiğim tarzda yazılmış bir feed ekranı aşağıda. Önemli kısımlar açıkça işaretlendiği için varsayılan main actor izolasyonu açık da olsa kapalı da olsa çalışıyor:
- ```swift
- import SwiftUI
- import Observation
- nonisolated struct Post: Decodable, Sendable, Identifiable {
- let id: Int
- let title: String
- }
- nonisolated struct FeedAPI: Sendable {
- func fetchData() async throws -> Data {
- let url = URL(string: "https://example.com/feed.json")!
- let (data, _) = try await URLSession.shared.data(from: url)
- return data
- }
- }
- @concurrent
- func decodePosts(from data: Data) async throws -> [Post] {
- try JSONDecoder().decode([Post].self, from: data) // main actor dışında
- }
- @MainActor
- @Observable
- final class FeedModel {
- private(set) var posts: [Post] = []
- private(set) var errorMessage: String?
- private let api = FeedAPI()
- func refresh() async {
- do {
- let data = try await api.fetchData()
- posts = try await decodePosts(from: data) // yeniden main actor'de, dispatch yok
- } catch {
- errorMessage = error.localizedDescription
- }
- }
- }
- struct FeedView: View {
- @State private var model = FeedModel()
- var body: some View {
- List(model.posts) { Text($0.title) }
- .task { await model.refresh() } // iOS 15+
- .refreshable { await model.refresh() } // iOS 15+
- }
- }
- ```
- Ne `DispatchQueue.main` var ne de `MainActor.run`. Model `@MainActor` olduğu için `posts`'a atama yapmak tasarım gereği güvenli. Bir tipte `nonisolated` kullanmak (SE-0449, Swift 6.1) onu modülün varsayılan main actor izolasyonunun dışında tutuyor, böylece main actor dışında rahatça kullanılabiliyor.
- ### Callback köprüleri: MainActor.run mı, @MainActor closure mı
- Bazen yine de bir köprüye ihtiyacınız oluyor, genelde kontrolünüzde olmayan completion handler'lı bir API'den gelirken:
- - **Senkron** bir callback içinde `Task { @MainActor in ... }` kullanın.
- - **İzole olmayan bir async** fonksiyonda `await MainActor.run { ... }` (iOS 13+) çalışır. Ama genelde daha temiz çözüm, çağırdığınız fonksiyonu `@MainActor` yapıp doğrudan `await` etmek.
- - Bir callback'in main thread'de geldiği *belgelenmişse*, `MainActor.assumeIsolated { ... }` (iOS 13+) bunu derleyiciye söylüyor. Varsayım yanlışsa uygulamayı fatal error ile durduruyor.
- ## [weak self] ne zaman hâlâ önemli
- Bir `Task`, closure'ını ve closure'ın yakaladığı her şeyi task bitene kadar tutuyor. Sonra bırakıyor. Bir saniyede biten bir istek için `self`'i strong yakalamak yalnızca nesnenin biraz daha uzun yaşaması demek. Bu bir leak değil, yani oradaki `[weak self]` çoğunlukla gürültü.
- Task kendi kendine hiç bitmediğinde gerçek bir soruna dönüşüyor:
- ```swift
- final class TickerViewController: UIViewController {
- private let service = PriceService()
- private let priceLabel = UILabel()
- private var tickerTask: Task<Void, Never>?
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- let stream = service.prices() // AsyncStream<Price>
- tickerTask = Task { [weak self] in
- for await price in stream {
- guard let self else { return } // sadece bu iterasyon boyunca strong
- self.priceLabel.text = price.value.formatted()
- }
- }
- }
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- tickerTask?.cancel() // döngüyü bitirir
- tickerTask = nil
- }
- }
- ```
- Task'ın sahibi view controller. Sonsuz bir döngü `self`'i strong yakalasaydı onu sonsuza kadar hayatta tutardı. İki detay önemli. `guard let self` döngünün *içinde*, yani `self` yalnızca bir eleman işlenirken tutuluyor. Task da açıkça iptal ediliyor. SwiftUI'da `.task { for await ... }` tercih edin: view kaybolduğunda otomatik iptal ediliyor.
- ## Tuzaklar
- - **Task'ın en başında `guard let self`.** `Task { [weak self] in guard let self else { return }; for await ... }` yazmak `self`'i bütün döngü boyunca strong yapıyor, weak capture'ın hiçbir etkisi kalmıyor.
- - **Sadece içteki closure'da weak.** `loader.load { result in Task { @MainActor [weak self] in ... } }` yazınca dıştaki closure `self`'i yine strong yakalıyor. Yeni derleyiciler tam olarak bu durum için uyarı veriyor, örneğin Swift 6.4. `[weak self]`'i en dıştaki escaping closure'a koyun.
- - **`async`'i arka plan sanmak.** `NonisolatedNonsendingByDefault` açıkken açıkça öyle değil. CPU'yu yoran iş için `@concurrent` kullanın. Ben bunu `Task.detached`'a tercih ediyorum. Detached task, iptalini sizin yönetmeniz gereken unstructured bir task oluşturuyor ve Apple'ın dokümantasyonu da structured concurrency işi modelleyebiliyorsa detached task'tan kaçınmayı öneriyor.
- - **`@MainActor` kod içinde `DispatchQueue.main.async`.** Zaten main actor'desiniz. Dispatch işi sadece run loop'un sonraki turuna erteliyor ve actor bilgisini derleyiciden saklıyor.
- - **Varsayılan main actor izolasyonunu her yerde açmak.** Uygulama ve UI kodu için tasarlandı. Bir networking paketinde, hiç main actor'e ait olmayan işleri main actor'e itiyor.
- ## Kontrol listesi
- 1. UI tipleri `@MainActor`. Uygulama target'larında `Default Actor Isolation = MainActor` ayarını düşünün (Swift 6.2+).
- 2. `NonisolatedNonsendingByDefault`'u (ya da Xcode'daki Approachable Concurrency ayarını) açın ve CPU'yu yoran fonksiyonları `@concurrent` ile işaretleyin.
- 3. `DispatchQueue.main.async` yerine main actor metotlarını `await` ile çağırın. `Task { @MainActor in }`'i yalnızca callback sınırlarında kullanın.
- 4. Kısa task'larda refleks `[weak self]`'i bırakın. Uzun ömürlü döngülerde iptalle birlikte koruyun.
- 5. SwiftUI'da elle saklanan task'lar yerine `.task {}` ve `.task(id:)` (iOS 15+) kullanın.
- GCD dönemindeki kurallar kendi zamanları için doğruydu. Swift 6.2 ile bu kuralların çoğunu derleyici sizin yerinize uygulayabiliyor, yeter ki kodunuzun nerede çalışması gerektiğini ona söyleyin.