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 Concurrency: Guida MainActor 2026

Aggiornato: 3 settembre 2026

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.

Hiroshi Sato
Sull'Autore Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.