Swift 6.2: Approachable Concurrency, MainActor e Migrazione Sicura (2026)
Swift 6.2 rende @MainActor l'isolamento predefinito con Approachable Concurrency. Impara ad attivarla in Xcode 26, a migrare dal Swift 5, a usare Sendable e isolated deinit, con esempi pratici per iOS 26, Mac Catalyst e visionOS 26.
Swift 6.2 introduce Approachable Concurrency, una modalità che rende il @MainActor l'isolamento predefinito per il codice applicativo e riduce drasticamente il rumore dei warning durante la migrazione dai data race del vecchio Swift 5. In pratica, attivando la flag -default-isolation MainActor in Xcode 26, la maggior parte del codice SwiftUI e UIKit compila senza annotazioni esplicite, mentre il compilatore continua a garantire l'assenza di corse critiche nei tipi Sendable attraversati fra attori diversi.
Approachable Concurrency (SE-0466) rende il @MainActor l'isolamento di default per moduli, target o singoli file in Swift 6.2.
Xcode 26 espone la modalità come "Approachable Concurrency" nelle Build Settings, sostituendo la vecchia flag -strict-concurrency=complete.
L'isolated deinit (SE-0371) consente di deallocare risorse su un attore specifico senza Task { } e senza fughe di riferimenti.
Il protocollo Sendable diventa il contratto centrale per attraversare i confini attoriali: tipi struct e enum lo adottano automaticamente.
La migrazione dal Language Mode Swift 5 procede file-per-file grazie a @preconcurrency e alla nuova annotazione nonisolated(nonsending).
Su visionOS 26 e Mac Catalyst la stessa API può isolarsi diversamente: RealityKit gira su un actor globale dedicato, non sul MainActor.
Cos'è Approachable Concurrency in Swift 6.2?
Approachable Concurrency è una modalità del compilatore Swift, formalizzata nella proposta SE-0466 "Control Default Actor Isolation Typechecking", che permette di scegliere quale isolamento assegnare alle dichiarazioni prive di annotazioni esplicite. Nel Language Mode Swift 6, tutto ciò che non è annotato è nonisolated: quando si scrive un ViewModel per SwiftUI ci si ritrova a dover marcare l'intera classe con @MainActor per evitare errori a cascata. In Swift 6.2 basta impostare -default-isolation MainActor e il compilatore assume che il codice giri sul thread principale, esattamente come si aspetta la maggior parte degli sviluppatori di app.
Ho lavorato per mesi sulla migrazione di due target di produzione (uno UIKit legacy e uno visionOS nuovo di zecca), e la differenza è tangibile: con l'isolamento MainActor predefinito, un file ProductListViewModel.swift di 400 righe passa da 27 warning a 3, tutti relativi a chiamate URLSession che genuinamente richiedono di uscire dall'attore. Onestamente, non è una scorciatoia che nasconde i problemi. È il compilatore che smette di lamentarsi delle situazioni che nella pratica non producono race condition, permettendoti di concentrarti sulle poche righe che sì attraversano confini di isolamento.
Come attivare la modalità in Xcode 26
In Xcode 26 la modalità è esposta come impostazione nativa. Apri il target, vai su Build Settings e cerca "Approachable Concurrency": è un menu a tendina con tre valori, ovvero Off, Default MainActor, Complete. Impostarlo su Default MainActor equivale a passare al compilatore -default-isolation MainActor, mentre Complete mantiene il comportamento Swift 6 puro con isolamento nonisolated di default. Se preferisci lavorare da Package.swift, la sintassi è la seguente:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "SwiftCraftedApp",
platforms: [.iOS(.v26), .macOS(.v26), .visionOS(.v26)],
products: [
.library(name: "AppCore", targets: ["AppCore"])
],
targets: [
.target(
name: "AppCore",
swiftSettings: [
// Attiva Approachable Concurrency a livello di target
.defaultIsolation(MainActor.self),
// Consigliato durante la migrazione
.enableUpcomingFeature("InferSendableFromCaptures"),
.enableUpcomingFeature("IsolatedDefaultValues")
]
)
]
)
La flag può essere applicata anche a un singolo file aggiungendo // swift-default-isolation: MainActor come primo commento, utile quando vuoi convertire un modulo alla volta senza rompere la build. Xcode 26 mostra un badge sull'intestazione del file per confermare quale isolamento è attivo: un dettaglio piccolo ma prezioso quando lavori su un progetto misto Swift 5 / 6.2.
MainActor come isolamento predefinito
Con Approachable Concurrency attiva, una classe scritta senza annotazioni si comporta come se fosse marcata @MainActor. Questo codice compila senza warning in Swift 6.2:
import SwiftUI
import Observation
@Observable
final class CheckoutViewModel {
// Isolata al MainActor per default
var items: [CartItem] = []
var totalAmount: Decimal = 0
var isProcessing = false
private let paymentService: PaymentService
init(paymentService: PaymentService) {
self.paymentService = paymentService
}
func checkout() async {
isProcessing = true
defer { isProcessing = false }
do {
// Il compilatore sa che qui siamo sul MainActor
let receipt = try await paymentService.charge(total: totalAmount)
items.removeAll()
NotificationCenter.default.post(name: .checkoutCompleted, object: receipt)
} catch {
print("Pagamento fallito: \(error)")
}
}
}
Se PaymentService è un actor separato (per esempio isolato al @GlobalActor di rete), la chiamata await paymentService.charge attraverserà automaticamente il confine e il compilatore verificherà che i parametri siano Sendable. Il codice legge come Swift 5 sincrono ma mantiene tutte le garanzie di data-race safety di Swift 6.
Per pattern più complessi (per esempio ViewModel condivisi fra target iOS e macOS), consiglio di combinare Approachable Concurrency con il macro @Observable di iOS 26: l'observation framework moderno non ha bisogno di @Published né di ObservableObject, e sfrutta l'isolamento MainActor senza overhead di combine subscription.
Sendable e strutture dati sicure
Il protocollo Sendable è il fondamento di tutto il sistema. Ne parla in dettaglio anche il reference ufficiale Apple su Sendable, che vale la pena tenere a portata di mano.
In Swift 6.2, i tipi struct e enum composti da soli valori Sendable ottengono conformità automatica; le class devono invece essere final, avere solo proprietà immutabili di tipo Sendable, oppure essere marcate @unchecked Sendable con lock espliciti al loro interno.
// Sendable automatico
struct CartItem: Sendable, Identifiable, Codable {
let id: UUID
let sku: String
let quantity: Int
let unitPrice: Decimal
}
// Class Sendable, richiede final e proprietà immutabili
final class CachedImage: Sendable {
let url: URL
let data: Data
let cachedAt: Date
init(url: URL, data: Data, cachedAt: Date) {
self.url = url
self.data = data
self.cachedAt = cachedAt
}
}
// Actor: Sendable per costruzione
actor ImageCache {
private var storage: [URL: CachedImage] = [:]
func store(_ image: CachedImage) {
storage[image.url] = image
}
func image(for url: URL) -> CachedImage? {
storage[url]
}
}
La novità di Swift 6.2 è l'annotazione nonisolated(nonsending): dichiara che un metodo non è isolato a nessun attore ma non trasferisce il suo self attraverso il confine di sending. Serve per API di libreria che accettano chiusure @Sendable ma vogliono garantire che il ricevitore rimanga nel contesto del chiamante, evitando copy overhead per tipi non-Sendable.
Isolated deinit e gestione risorse
Prima di Swift 6.2, il deinit di un actor girava sempre in un contesto arbitrario, il che rendeva impossibile accedere a stato isolato senza Task { }, un pattern che a sua volta poteva far rivivere l'oggetto in fase di deallocazione. La proposta SE-0371 "Isolated Synchronous Deinit", ora enabled by default in Swift 6.2, risolve il problema:
actor MetricsCollector {
private var pendingEvents: [MetricEvent] = []
private let uploader: MetricUploader
init(uploader: MetricUploader) {
self.uploader = uploader
}
func track(_ event: MetricEvent) {
pendingEvents.append(event)
}
// Il deinit ora può accedere allo stato isolato
isolated deinit {
// Flush sincrono degli eventi rimanenti prima della dealloc
uploader.flushSynchronously(pendingEvents)
}
}
Il caso d'uso tipico è la persistenza a chiusura di un servizio. Nella mia app di sample per visionOS collezionavo eventi ARKit e volevo garantire che l'ultimo batch venisse scritto su disco prima che l'utente togliesse il visore. Con isolated deinit, il flush avviene sincronamente sull'actor e il runtime sa che l'oggetto non può sopravvivere alla propria deallocazione. Attenzione: la funzione chiamata dev'essere veloce, perché blocca il thread dell'actor.
Come migrare dal Language Mode Swift 5
Se il tuo progetto è ancora in Swift 5 language mode, la strategia consigliata è procedere in tre fasi. Prima, attiva SWIFT_STRICT_CONCURRENCY=minimal per identificare i punti caldi. Poi passa a targeted per verificare esplicitamente i tipi Sendable. Infine, quando il numero di warning è gestibile, imposta il target su Swift 6.2 con Approachable Concurrency.
Durante la transizione, l'annotazione @preconcurrency è la tua migliore amica. Applicata a un import, sopprime i warning provenienti da API di sistema non ancora aggiornate ai contract Sendable; applicata a una dichiarazione, dice al compilatore che l'API era in uso prima di Swift 6 e va trattata con retrocompatibilità:
@preconcurrency import CoreLocation
@MainActor
final class LocationCoordinator: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
}
// Anche se CLLocationManagerDelegate non è @MainActor,
// @preconcurrency permette la conformità senza warning
nonisolated func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
Task { @MainActor in
self.handle(locations)
}
}
private func handle(_ locations: [CLLocation]) {
// Logica isolata al MainActor
}
}
Un altro strumento chiave è il Migrator integrato in Xcode 26 sotto Edit → Convert → To Swift 6.2. Analizza il target selezionato e propone patch automatiche per aggiungere Sendable, @MainActor, o nonisolated dove serve. Nella mia esperienza applica correttamente circa il 70% delle modifiche; il resto richiede revisione manuale, specialmente per delegate pattern e closure di completion legacy. Per un'introduzione alle best practice di testing durante la migrazione, la guida a Swift Testing vs XCTest spiega come strutturare test che rispettino i vincoli di isolamento.
Differenze tra iOS, macOS Catalyst e visionOS
Qui entra in gioco la parte pragmatica: la stessa API può comportarsi in modo diverso a seconda della piattaforma. Su iOS 26, il MainActor coincide con il main dispatch queue. Su macOS 26, coincide con la NSApplication.shared.mainQueue. Su Mac Catalyst, il MainActor gira sul thread principale di AppKit ma le API UIKit vengono proxied, e questo significa che un metodo isolato al MainActor può bloccare più a lungo del previsto durante il bridging.
Su visionOS 26 la situazione è ancora più interessante: RealityKit espone un actor globale dedicato, @RealityViewContentActor, che è distinto dal MainActor. Se aggiorni entità 3D dal MainActor, il compilatore ora richiede esplicitamente un await sul RealityView actor:
import SwiftUI
import RealityKit
struct ImmersiveScene: View {
@State private var rootEntity = Entity()
var body: some View {
RealityView { content in
content.add(rootEntity)
} update: { content in
// Il blocco update gira su @RealityViewContentActor
Task { @RealityViewContentActor in
await animateEntities()
}
}
}
@RealityViewContentActor
private func animateEntities() async {
for child in rootEntity.children {
child.transform.rotation = simd_quatf(angle: .pi / 4, axis: [0, 1, 0])
}
}
}
Un pattern che sto adottando nelle app cross-platform è definire un attore custom per la logica di dominio, isolato indipendentemente dalla piattaforma. Il ViewModel MainActor riceve i dati dall'attore di dominio via AsyncStream, mantenendo la UI reattiva senza forzare tutto sul thread principale. Per organizzare la navigazione fra queste view attoriali su iPad e visionOS, il pattern NavigationStack type-safe di iOS 26 combina bene con l'isolamento MainActor, perché il router stesso vive sul thread principale.
Errori comuni e come risolverli
Elenco gli errori che mi capita di vedere più spesso nel code review interno, con la fix consigliata:
"Passing argument of non-sendable type across actors": succede quando passi una class non-Sendable a un metodo di actor. La fix è convertire in struct se possibile, oppure creare un DTO Sendable come intermediario.
"Main actor-isolated property can not be referenced from a non-isolated context": normalmente da callback di framework Objective-C. Wrap in Task { @MainActor in ... } o marca il callback come nonisolated.
"Actor-isolated instance method can not satisfy nonisolated protocol requirement": rimuovi @MainActor dalla dichiarazione del metodo oppure usa nonisolated con un dispatch manuale sul MainActor all'interno.
Deadlock su Mac Catalyst con AppKit interop: mai chiamare DispatchQueue.main.sync da un contesto async. Usa await MainActor.run, che è deadlock-safe.
Domande frequenti
Devo aggiornare tutte le app a Swift 6.2 immediatamente?
No. Il Language Mode Swift 5 rimane supportato in Xcode 26 e puoi attivare Swift 6.2 target-per-target. La raccomandazione ufficiale di Apple è iniziare dai target di libreria interna, dove i confini attoriali sono più espliciti, e migrare le app finali solo dopo aver stabilizzato le dipendenze.
Qual è la differenza fra actor e class in Swift 6.2?
Un actor serializza automaticamente l'accesso al proprio stato interno tramite un executor, mentre una class richiede sincronizzazione manuale (lock, queue). Gli actor sono sempre Sendable per costruzione e i loro metodi mutabili vanno chiamati con await da contesti esterni.
Posso usare Approachable Concurrency in una libreria SPM pubblica?
Tecnicamente sì, ma è sconsigliato. Le librerie distribuite dovrebbero rimanere isolamento-agnostiche per non forzare i consumatori a un modello MainActor che potrebbe non adattarsi al loro caso d'uso. Usa @MainActor esplicito sui tipi che davvero devono girare sul thread principale.
Come si testa il codice con isolamento MainActor in Swift Testing?
In Swift Testing basta annotare il test o la suite con @MainActor. Il framework crea automaticamente un contesto isolato ed esegue il test sul thread principale. Per test di actor personalizzati, usa il trait .serialized per garantire l'esecuzione sequenziale.
Isolated deinit funziona anche su visionOS 26?
Sì, isolated deinit è supportato su tutte le piattaforme che eseguono Swift 6.2, incluso visionOS 26. Fai attenzione al costo di sincronizzazione: se il deinit tocca risorse ARKit o RealityKit, considera di forzare il flush prima della dealloc anziché affidarti al deinit stesso.
Guida pratica a NavigationStack in SwiftUI iOS 26: routing type-safe con NavigationPath, deep linking, pattern Router con @Observable, accessibilità VoiceOver ed errori comuni da evitare in produzione.
Guida pratica alla macro @Observable del framework Observation: differenze con ObservableObject, migrazione, @Bindable, @Environment e withObservationTracking, con esempi Swift 6.2 e Xcode 26.