Se você já tentou adotar strict concurrency no Swift 6.0 ou 6.1, sabe bem a dor: dezenas de warnings, erros de Sendable por todo lado, e aquela sensação de que o compilador tá trabalhando contra você. Pois é — a própria equipe do Swift admitiu que foi longe demais, rápido demais. E o resultado disso? O Swift 6.2 chega com o conceito de Approachable Concurrency — concorrência acessível — um conjunto de mudanças que tornam o modelo de concorrência bem mais fácil de usar, sem abrir mão da segurança.
Honestamente, essa é provavelmente a mudança mais bem-vinda no Swift nos últimos dois anos.
Neste guia, vamos passar por cada uma das três grandes mudanças — nonisolated(nonsending) como padrão, o novo atributo @concurrent, e o MainActor como isolamento padrão de módulo — com exemplos práticos, tabela de referência e um guia de migração passo a passo.
O Problema: Concorrência no Swift 6.0 e 6.1
Pra entender por que o Swift 6.2 existe, vale a pena revisitar o que estava quebrado. O modelo de concorrência do Swift 6.0 introduziu uma inconsistência fundamental no comportamento de funções nonisolated.
A Inconsistência das Funções nonisolated
No Swift 6.0 e 6.1, funções marcadas como nonisolated tinham comportamentos completamente diferentes dependendo de serem síncronas ou assíncronas:
- Funções síncronas
nonisolated: executavam no executor do chamador (se chamadas de um actor, rodavam naquele actor) - Funções assíncronas
nonisolated: eram despachadas para o executor global cooperativo (background), não importando quem as chamou
Essa diferença causava uma cascata de problemas. Olha esse exemplo:
// Swift 6.0/6.1 — Comportamento inconsistente
class ImageProcessor {
var cache: [String: UIImage] = [:]
// ✅ Síncrona nonisolated: herda o executor do chamador
nonisolated func getCachedImage(for key: String) -> UIImage? {
return cache[key]
}
// ⚠️ Assíncrona nonisolated: roda no executor GLOBAL (background!)
nonisolated func processImage(_ data: Data) async -> UIImage? {
// Mesmo chamada do MainActor, esta função roda em background
// Isso exige que 'data' seja Sendable
return UIImage(data: data)
}
}
Mas o impacto real dessa inconsistência aparecia no dia a dia com SwiftUI. Considere este cenário — extremamente comum, aliás:
// Swift 6.1 — Erro frustrante em código simples
class NetworkingClient {
func loadUserPhotos() async throws -> [Photo] {
let url = URL(string: "https://api.example.com/photos")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Photo].self, from: data)
}
}
struct SomeView: View {
let network = NetworkingClient()
var body: some View {
Text("Hello")
.task { await getData() }
}
func getData() async {
// ❌ Swift 6.1: ERROR — sending main actor-isolated
// 'self.network' to nonisolated callee risks causing
// data races
let photos = try? await network.loadUserPhotos()
}
}
O compilador reclamava porque loadUserPhotos(), sendo async e nonisolated, seria executada em background. Ou seja, network — uma propriedade isolada ao MainActor por estar numa View — estava sendo "enviada" para outro contexto de isolamento.
Pra resolver, você precisava marcar NetworkingClient como Sendable, torná-la um actor, ou sair adicionando anotações por toda parte. Pra uma classe simples de rede, isso era absurdamente burocrático. Já passei por isso mais vezes do que gostaria de admitir.
O Que Mudou no Swift 6.2
O Swift 6.2, anunciado junto com o Xcode 26, traz três mudanças principais que formam o pilar da Approachable Concurrency:
nonisolated(nonsending)como padrão — Funções assíncronasnonisolatedagora herdam o executor do chamador por padrão, eliminando a inconsistência@concurrent— Novo atributo explícito para quando você realmente quer que uma função rode em backgrounddefaultIsolation(MainActor.self)— Possibilidade de definir oMainActorcomo isolamento padrão de todo o módulo, parecido com o que o SwiftUI já faz com@MainActornas Views
A filosofia por trás é simples: o código seguro e previsível deve ser o padrão; a concorrência explícita deve ser a exceção.
nonisolated(nonsending): Herdar o Executor do Chamador
Essa é, sem dúvida, a mudança mais impactante do Swift 6.2. O atributo nonisolated(nonsending) — agora o comportamento padrão para funções assíncronas nonisolated — faz com que a função rode no mesmo executor de quem a chamou.
Parece uma mudança pequena, mas as implicações são enormes.
Como Funcionava Antes (Swift 6.1)
// Swift 6.1
// Função async nonisolated → roda no executor global (background)
func fetchData() async -> Data {
// Sempre roda em background, mesmo chamada do MainActor
// Exige que argumentos sejam Sendable
return Data()
}
Como Funciona Agora (Swift 6.2)
// Swift 6.2
// Função async nonisolated → herda o executor do chamador
func fetchData() async -> Data {
// Chamada do MainActor? Roda no MainActor.
// Chamada de background? Roda em background.
// Sem exigência de Sendable!
return Data()
}
O Exemplo do NetworkingClient — Resolvido
Lembra daquele erro frustrante com NetworkingClient? No Swift 6.2, ele simplesmente funciona:
// Swift 6.2 — Funciona sem alterações!
class NetworkingClient {
func loadUserPhotos() async throws -> [Photo] {
let url = URL(string: "https://api.example.com/photos")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Photo].self, from: data)
}
}
struct SomeView: View {
let network = NetworkingClient()
var body: some View {
Text("Hello")
.task { await getData() }
}
func getData() async {
// ✅ Swift 6.2: Funciona perfeitamente!
// loadUserPhotos() herda o MainActor do chamador.
// Não há "envio" para outro contexto — tudo roda
// no mesmo executor.
let photos = try? await network.loadUserPhotos()
}
}
Por que funciona? Porque loadUserPhotos() agora herda o executor do chamador. Como getData() roda no MainActor (por estar numa View), loadUserPhotos() também roda no MainActor. Sem transferência de contexto, sem exigência de Sendable. Simples assim.
Usando nonisolated(nonsending) Explicitamente
Se quiser ser explícito (ou se estiver num módulo que ainda não ativou a feature flag), dá pra usar o atributo diretamente:
// Uso explícito do atributo
nonisolated(nonsending) func processData(_ data: Data) async -> Result {
// Herda o executor do chamador
return process(data)
}
Detalhe importante: quando uma função nonisolated(nonsending) é chamada de fora de qualquer actor (por exemplo, de uma Task.detached), ela roda no executor global cooperativo, exatamente como antes. A mudança só afeta o comportamento quando há um actor no contexto de chamada.
@concurrent: Quando Você Realmente Precisa de Background
Ok, mas se o novo padrão faz tudo rodar no executor do chamador, como executar trabalho pesado em background? É aí que entra o @concurrent.
O @concurrent é basicamente o oposto de nonisolated(nonsending): ele garante que a função será despachada para o executor global cooperativo, não importa quem chamou. É o comportamento antigo, mas agora opt-in em vez de padrão.
Exemplo Prático: Decodificação de JSON
Decodificação de JSON é um caso clássico de trabalho que pode ser pesado e que se beneficia de rodar em background:
class Networking {
func getFeed() async throws -> Feed {
// loadData herda o executor do chamador (novo padrão)
let data = try await loadData(from: Feed.endpoint)
// decode roda EXPLICITAMENTE em background
return try await decode(data)
}
@concurrent
func decode<T: Decodable>(_ data: Data) async throws -> T {
// Sempre roda no executor global cooperativo (background)
// Ideal para trabalho CPU-intensivo
return try JSONDecoder().decode(T.self, from: data)
}
func loadData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
Repare no equilíbrio aqui: loadData é uma operação de I/O que passa a maior parte do tempo em await — não faz sentido forçá-la para background. Já decode é CPU-intensiva e realmente se beneficia de rodar fora da thread principal.
Quando Usar @concurrent
Use @concurrent quando a função realiza trabalho computacional significativo que poderia bloquear a thread principal:
- Decodificação/codificação de grandes volumes de dados (JSON, Protobuf)
- Processamento de imagens
- Cálculos matemáticos intensivos
- Compressão/descompressão de dados
- Operações criptográficas
Na prática, se a função fica ocupada fazendo cálculo entre os pontos de await, ela é candidata a @concurrent.
Combinações Inválidas
O compilador impede combinações que não fazem sentido lógico (e ainda bem):
// ❌ Combinações inválidas — erros de compilação
// @concurrent com @MainActor: isolamento conflitante
@concurrent @MainActor func decode(_ data: Data) async -> Feed {
return try! JSONDecoder().decode(Feed.self, from: data)
}
// @concurrent com nonisolated(nonsending): isolamento conflitante
@concurrent nonisolated(nonsending) func decode(_ data: Data) async -> Feed {
return try! JSONDecoder().decode(Feed.self, from: data)
}
Regra simples: @concurrent é mutuamente exclusivo com qualquer forma de isolamento a um actor ou com nonisolated(nonsending). Faz sentido — você não pode dizer "roda aqui" e "roda lá" ao mesmo tempo.
defaultIsolation: MainActor Como Padrão do Módulo
A terceira mudança permite definir o MainActor como isolamento padrão de todo o módulo. Na prática, todas as classes, structs, funções e propriedades do módulo ficam automaticamente isoladas ao MainActor, a menos que você marque explicitamente de outra forma.
Pra apps, isso faz todo sentido. A maior parte do código de um aplicativo interage com a UI e precisa rodar na thread principal. Em vez de espalhar @MainActor por dezenas de tipos, você configura uma vez e pronto.
Configuração no Package.swift
// Package.swift
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MeuApp",
targets: [
.executableTarget(
name: "MeuApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
]
)
Com essa configuração, todo o código dentro do target "MeuApp" é implicitamente @MainActor. Tipos que precisam rodar fora da main thread podem ser marcados com nonisolated.
Configuração no Xcode 26
No Xcode 26, a configuração fica em Build Settings:
- Abra as configurações do target
- Busque por "Default Actor Isolation"
- Defina como MainActor
Pra novos projetos criados no Xcode 26, essa já é a configuração padrão. Um alívio, sinceramente.
Como Ativar no Xcode 26 e em Swift Packages
A ativação da Approachable Concurrency depende do seu ambiente. Vamos ver as duas formas principais.
No Xcode 26
Pra projetos Xcode, basta ajustar duas configurações em Build Settings:
- Busque por "Approachable Concurrency" e defina como Yes
- Busque por "Default Actor Isolation" e defina como MainActor
A primeira ativa o nonisolated(nonsending) como padrão. A segunda define o MainActor como isolamento padrão do módulo. Dá pra ativar uma sem a outra — depende da sua estratégia de migração.
Em Swift Packages
Pra pacotes Swift, configure no Package.swift:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MeuPacote",
targets: [
.target(
name: "MeuPacote",
swiftSettings: [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
)
]
)
As feature flags permitem adoção gradual:
NonisolatedNonsendingByDefault— ativa o novo comportamento padrão de funções async nonisolatedInferIsolatedConformances— permite que conformances a protocolos herdem o isolamento do tipo, reduzindo anotações explícitas.defaultIsolation(MainActor.self)— define o MainActor como isolamento padrão do módulo
Uma observação importante: se seu pacote é uma biblioteca (não um app), pense com cuidado antes de usar defaultIsolation(MainActor.self). Bibliotecas frequentemente fazem trabalho que não tem relação com a UI, e forçar MainActor pode criar problemas inesperados pra quem consome o pacote.
Tabela de Referência Rápida
Guarda essa tabela — ela é bem útil na hora de decidir qual atributo usar:
| Atributo | Onde Executa | Exige Sendable? | Quando Usar |
|---|---|---|---|
nonisolated(nonsending) |
Executor do chamador | Não | Padrão no Swift 6.2. Ideal pra maioria das funções async. |
@concurrent |
Executor global (background) | Sim | Trabalho CPU-intensivo que deve sair da thread principal. |
@MainActor |
Main thread | Sim (para argumentos externos) | Código que interage com a UI ou precisa da thread principal. |
nonisolated (explícito) |
Depende do contexto | Depende | Pra opt-out do isolamento padrão em tipos específicos. |
Guia de Migração: Do Swift 6.1 para o 6.2
A migração pro Swift 6.2 pode (e deve) ser feita de forma gradual. Aqui vai uma abordagem em fases que tem funcionado bem:
Fase 1: Ativar Approachable Concurrency
Comece ativando apenas o NonisolatedNonsendingByDefault. Essa mudança vai eliminar muitos dos erros de Sendable que você tinha antes, mas pode introduzir novos warnings em funções que dependiam do comportamento antigo.
// Fase 1: apenas o novo padrão
.target(
name: "MeuTarget",
swiftSettings: [
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
]
)
Fase 2: Adicionar @concurrent Onde Necessário
Após ativar a feature, compile o projeto e observe os novos warnings. Funções que devem rodar em background precisam ser marcadas com @concurrent:
// Antes (Swift 6.1) — rodava em background implicitamente
func processLargeDataset(_ data: Data) async -> ProcessedData {
// Trabalho pesado que PRECISA de background
return heavyComputation(data)
}
// Depois (Swift 6.2) — background explícito com @concurrent
@concurrent
func processLargeDataset(_ data: Data) async -> ProcessedData {
// Agora explicitamente em background
return heavyComputation(data)
}
Fase 3: Ativar defaultIsolation (Opcional)
Se seu módulo é um app (não uma biblioteca), considere ativar o MainActor como isolamento padrão. Isso vai permitir remover dezenas de anotações @MainActor repetidas pelo código:
// Fase 3: isolamento padrão
.target(
name: "MeuApp",
swiftSettings: [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.defaultIsolation(MainActor.self)
]
)
Fase 4: Limpeza
Com tudo ativado, é hora da faxina no código:
- Remova anotações
@MainActorredundantes (agora são o padrão) - Remova conformances
Sendabledesnecessárias - Remova
@Sendablede closures que não precisam mais - Simplifique tipos que foram transformados em
actorsó pra satisfazer o compilador
Essa fase é surpreendentemente satisfatória. Tem algo de terapêutico em deletar anotações que nunca deveriam ter existido.
Erros Comuns e Como Evitá-los
1. Ativar a Feature Sem Revisar o Código
Esse é o erro mais perigoso. Se você ativar NonisolatedNonsendingByDefault sem revisar funções que faziam trabalho pesado em background, pode ter problemas sérios. No Swift 6.1, essas funções iam automaticamente para background. No 6.2, elas herdam o executor do chamador — que pode ser o MainActor.
// ⚠️ PERIGO: essa função fazia trabalho pesado em background no 6.1
// No 6.2, se chamada do MainActor, vai TRAVAR a UI!
func compressVideo(_ video: VideoAsset) async -> Data {
// Compressão pesada agora rodando na main thread 😬
return heavyCompression(video)
}
// ✅ SOLUÇÃO: marque explicitamente com @concurrent
@concurrent
func compressVideo(_ video: VideoAsset) async -> Data {
// Explicitamente em background ✅
return heavyCompression(video)
}
2. Usar @concurrent em Excesso
O impulso natural é marcar tudo com @concurrent pra "garantir". Resista a essa tentação — isso anula todos os benefícios da Approachable Concurrency e traz de volta os problemas de Sendable:
// ❌ NÃO faça isso — @concurrent desnecessário
@concurrent
func fetchUserName() async throws -> String {
// Apenas faz uma chamada de rede e espera
// Não há trabalho CPU-intensivo aqui
let (data, _) = try await URLSession.shared.data(from: userURL)
return String(data: data, encoding: .utf8) ?? ""
}
// ✅ Deixe sem @concurrent — o novo padrão é suficiente
func fetchUserName() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: userURL)
return String(data: data, encoding: .utf8) ?? ""
}
Regra prática: se a função passa a maior parte do tempo em await (esperando I/O), ela não precisa de @concurrent. Reserve o atributo pra trabalho que consome CPU de verdade entre os pontos de suspensão.
3. Não Entender Quando nonisolated(nonsending) Roda em Background
Lembre-se: nonisolated(nonsending) herda o executor do chamador. Se o chamador já está em background, a função também roda em background:
// Este código roda em background porque Task.detached não tem actor
Task.detached {
// Sem actor no contexto — executor global cooperativo
let result = await processData(data)
// processData roda em background aqui, mesmo sem @concurrent
}
Ou seja, o comportamento não é "roda sempre na main thread" — é "roda onde o chamador está". Uma distinção sutil mas importante.
4. Usar defaultIsolation em Bibliotecas
Evite defaultIsolation(MainActor.self) em bibliotecas compartilhadas. Consumidores da sua biblioteca podem não esperar que todas as funções rodem no MainActor, e isso pode gerar problemas de performance e deadlocks inesperados. Guarde essa configuração pra targets de app.
Perguntas Frequentes (FAQ)
O que é Approachable Concurrency no Swift 6.2?
Approachable Concurrency (concorrência acessível) é um conjunto de mudanças no Swift 6.2 que torna o modelo de concorrência mais fácil de usar. A principal mudança é que funções assíncronas nonisolated agora herdam o executor do chamador por padrão, eliminando a maioria dos erros de Sendable que frustavam desenvolvedores no Swift 6.0 e 6.1.
Qual a diferença entre nonisolated(nonsending) e @concurrent?
São opostos complementares. nonisolated(nonsending) faz a função herdar o executor do chamador — se chamada do MainActor, roda no MainActor; se chamada de background, roda em background. Não exige Sendable. Já @concurrent força a função a sempre rodar no executor global cooperativo (background), independente de quem chamou, e exige que argumentos sejam Sendable. Use o padrão pra maioria dos casos e @concurrent só pra trabalho CPU-intensivo.
Preciso migrar meu projeto existente para o Swift 6.2?
Não há obrigação, mas os incentivos são grandes. Se seu projeto sofre com erros de Sendable e você adicionou workarounds como @unchecked Sendable, a migração pro Swift 6.2 provavelmente vai simplificar muito seu código. A boa notícia é que dá pra fazer gradualmente — comece ativando NonisolatedNonsendingByDefault em um target por vez e adicione @concurrent onde for necessário.
O Swift 6.2 pode quebrar meu app existente?
Sim, potencialmente. A mudança mais crítica é que funções que antes rodavam em background automaticamente agora podem rodar na main thread. Se essas funções fazem trabalho pesado (processamento de imagens, compressão, cálculos complexos), sua UI pode travar. Por isso é essencial revisar o código ao ativar as novas features e adicionar @concurrent onde necessário.
Quando devo usar @concurrent em vez do padrão?
Use @concurrent quando a função realiza trabalho computacional significativo que poderia bloquear o executor do chamador. Os casos mais comuns: decodificação de grandes volumes de dados, processamento de imagens ou vídeos, cálculos matemáticos, operações criptográficas e compressão. Se a função passa a maior parte do tempo esperando I/O (chamadas de rede, leitura de disco), não use @concurrent — o padrão é mais adequado e mais simples.
Conclusão
O Swift 6.2 representa um amadurecimento importante do modelo de concorrência. Em vez de exigir que todo desenvolvedor domine conceitos de isolamento, Sendable e executores desde o primeiro dia, a linguagem agora oferece um caminho gradual: comece com o padrão seguro e simples, e adote complexidade só quando precisar.
O trio nonisolated(nonsending), @concurrent e defaultIsolation cria um modelo mental bem claro:
- Por padrão, funções async rodam onde você está (executor do chamador)
- Precisa de background? Peça explicitamente com
@concurrent - Seu módulo é um app? Defina
MainActorcomo padrão e esqueça anotações repetitivas
Minha recomendação: comece a migração hoje. Ative as features em um target de cada vez, revise funções que fazem trabalho pesado, e aproveite o código mais limpo que o Swift 6.2 possibilita. Depois de meses brigando com Sendable, essa mudança é um respiro.