← Todos os Posts

Concorrência no Swift 6.2 na prática: padrão MainActor, escape de propósito

O Swift 6.0 transformou data races em erro de compilação e, por um ano, fez todo mundo pagar por isso. O verificador de concorrência estrita transformou código de UI comum em um paredão de violações de Sendable e erros de “main actor-isolated property can not be referenced from a nonisolated context”. O diagnóstico estava correto (aquele código realmente podia ter race), mas o volume soterrou o sinal, e muitas equipes ou ficaram no modo Swift 5 ou foram salpicando @MainActor até os erros silenciarem.

O Swift 6.2 muda o padrão em vez das regras. A garantia de segurança contra data race é a mesma; o que mudou foi onde o compilador começa. Adote os novos padrões e a maior parte do paredão desaparece, porque o compilador agora assume o que já era verdade no seu app: a maior parte do seu código roda no main actor, e você o deixa de propósito, em lugares nomeados. Esse é o modelo que eu uso em produção na família de apps 9411, e ele é uma peça de um arco evolutivo maior que o resumo do What’s New in Swift de 2026 percorre através das propostas 6.3 e 6.4 que vieram depois. Veja como funciona, e os seis erros que ainda incomodam depois que você o ativa.

TL;DR

  • A SE-0466 permite padronizar um módulo inteiro para @MainActor. Defina SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor e pare de anotar cada view, model e view-model2.
  • A SE-0461 faz funções async nonisolated rodarem no actor do chamador por padrão (nonisolated(nonsending)), então chamar código async não força mais um salto de actor nem os erros de Sendable na travessia de fronteira que vinham junto3.
  • @concurrent é a válvula de escape. Marque uma função como @concurrent para empurrar trabalho pesado de CPU (decodificação, processamento de imagem) para uma thread em segundo plano, de forma deliberada e visível4.
  • O Xcode 26 ativa ambos os padrões para novos projetos. A chave guarda-chuva é SWIFT_APPROACHABLE_CONCURRENCY = YES5.
  • O modelo inverte o ônus: em vez de provar que cada linha é segura para rodar fora do main, você mantém tudo no main e prova os poucos lugares onde sai.
  • Seis erros concretos sobrevivem à mudança. Todos os seis têm correções de uma linha assim que você enxerga o padrão.

A inversão, e por que ela importa

Modelo antigo (Swift 6.0): o código é nonisolated até você isolá-lo. Todo tipo que tocava em estado de UI precisava de @MainActor, toda chamada async através de uma fronteira de isolamento precisava de conformidade com Sendable, e o compilador sinalizava cada lacuna. Para um app em que 95 por cento do código já roda na thread principal, você gastava seu tempo anotando os 95 por cento para descrever um fato que nunca esteve em dúvida.

Modelo novo (Swift 6.2): o código está no main actor até você sair dele. A SE-0466 permite declarar o isolamento de main actor como padrão do módulo, então uma view, seu model e seus helpers são todos @MainActor sem uma única anotação2. A SE-0461 então remove o segundo imposto: uma função async nonisolated agora roda em qualquer actor que a chamou em vez de saltar para o executor global, então fazer await nela não arrasta você através de uma fronteira de isolamento nem exige Sendable em tudo que está em escopo3.

O modelo mental é o que corresponde a como apps de UI realmente se comportam. Main-thread por padrão não é um meio-termo; é a verdade de um app cujo estado são suas views. A concorrência passa a ser a exceção que você busca, nomeada e contida, em vez da condição ambiente contra a qual você se defende em cada linha. A função do compilador inverte de “prove que isto é seguro para rodar concorrentemente” para “você disse que isto roda concorrentemente, então prove que é seguro”, e a segunda pergunta é feita em muito menos lugares.

A equipe central do Swift já disse o mesmo. No Swift Group Lab da WWDC 2026, Holly descreveu como, projetando o modelo de concorrência hoje, funções async nonisolated teriam rodado no contexto do chamador desde o primeiro dia; ela parafraseou que se apegou ao modelo de pool concorrente global por muito tempo e foi convencida do contrário por problemas em projetos do mundo real. Doug acrescentou que o padrão antigo empurrava tipos demais em direção a Sendable. (Ambas as citações são paráfrases de uma gravação de lab transcrita localmente; a Apple não publicou legendas para a sessão.)9

Ativando o recurso

Duas build settings, ambas nos padrões do Xcode 26 para novos projetos e ambas dignas de serem definidas explicitamente em um projeto existente5:

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

A primeira é a SE-0466: o módulo passa a ter o isolamento de main actor como padrão. A segunda é o guarda-chuva que habilita o conjunto de recursos de approachable-concurrency, incluindo o comportamento “caller-runs” da SE-0461. Em um Swift package, você define os mesmos padrões através de swiftSettings com as flags de upcoming-feature correspondentes, em vez da build setting do Xcode6.

Ative ambas em um projeto existente e a contagem de erros despenca, porque a maior parte do que o verificador estava sinalizando era código main-thread que ele antes não conseguia assumir como main-thread. O que resta é uma lista curta de casos de fronteira genuínos. Vale a pena conhecê-los pelo nome, porque cada um é um lugar onde seu código realmente sai do main actor, e a correção é dizer isso com precisão.

Os seis erros que sobrevivem à mudança

Estes são os erros de concorrência estrita que ainda aparecem sob SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, retirados da migração dos apps da família 941 para o modelo1. Cada um é uma fronteira real, não um falso positivo, e é por isso que a correção é uma anotação precisa em vez de uma supressão.

1. Funções puras em tipos Sendable. Um método puro em um enum Sendable (um construtor de URL, um formatador) herda o isolamento de main actor sob o padrão do módulo, e então dá erro quando chamado de um contexto nonisolated: “Call to main actor-isolated instance method in a synchronous nonisolated context.” O método não toca em estado nenhum, então isolá-lo no main actor está errado. Marque-o como nonisolated7:

nonisolated func searchURL(for query: String) -> URL? { ... }

2. Statics singleton em parâmetros padrão. static let shared = Foo() é main-actor-isolated sob o padrão, mas valores de parâmetros padrão são avaliados no contexto do chamador, que muitas vezes é nonisolated: “Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” Torne o static nonisolated. Se o tipo é Sendable (ou @unchecked Sendable porque você mesmo protege seu estado), você não precisa de nenhum qualificador unsafe:

final class KeychainProxySecretStore: @unchecked Sendable {
    nonisolated static let shared = KeychainProxySecretStore()
}

3. Constantes primitivas imutáveis. O mesmo problema de parâmetro padrão atinge uma constante simples: um static let defaultInterval referenciado a partir de um argumento padrão nonisolated. A correção é idêntica e a constante é trivialmente segura para compartilhar:

nonisolated static let defaultInterval: TimeInterval = 15 * 60

4. Um corpo de Task lendo self capturado. Uma closure externa captura [weak self]; dentro dela, um Task { @MainActor in self?.foo() } lê esse optional capturado: “Reference to captured var ‘self’ in concurrently-executing code.” A Task lê uma vinculação var do escopo envolvente de forma concorrente, e essa é a race. Recapture self na fronteira da Task para que a Task possua uma vinculação imutável:

NotificationCenter.default.addObserver(...) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.value = next
    }
}

5. Callbacks de KVO lendo estado de main actor. Um callback webView.observe(\.canGoBack) { wv, _ in ... } é @Sendable e portanto nonisolated, mas WKWebView.canGoBack é main-actor-isolated: “Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” O KVO entrega de forma síncrona na thread que mutou o valor, e o estado de navegação do WKWebView muta apenas na thread principal, então a leitura é sólida. Afirme isso com MainActor.assumeIsolated, que remove o salto de Task por completo e permanece síncrono8:

let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
    MainActor.assumeIsolated {
        guard let self else { return }
        // safe to read webView?.canGoBack synchronously
    }
}

assumeIsolated é uma promessa ao compilador, não uma pergunta. Use-o apenas onde a invariante de runtime genuinamente se sustenta (um callback documentado de main-thread), porque uma promessa errada é um crash, não um aviso.

6. Trabalho pesado que não deveria estar no main. Este o verificador não vai sinalizar, e é o mais importante de pegar por conta própria. Sob um padrão de main actor, um método síncrono limitado por CPU (decodificar um JSON de payload grande, redimensionar uma imagem) roda no main actor e trava sua UI. O padrão mantém você no main; @concurrent é como você sai de propósito4:

@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
    try JSONDecoder().decode(Report.self, from: data)
}

@concurrent transfere a função para o executor global e é mutuamente exclusivo com @MainActor, um global actor customizado e nonisolated(nonsending), por design: uma função ou roda onde seu chamador está ou roda deliberadamente para longe dele, nunca de forma ambígua4. A disciplina que o novo modelo pede vive inteiramente nesse padrão. Fique no main para tudo que toca a UI, e busque @concurrent apenas para o trabalho que comprovadamente precisa de uma thread em segundo plano.

Quando os padrões estão errados para você

Main-actor-por-padrão serve para apps: código SwiftUI e UIKit é majoritariamente main-thread, e o padrão corresponde à realidade. Ele serve menos bem em dois casos, e fingir o contrário desperdiça seu tempo.

  • Um alvo de biblioteca ou framework sem UI. Uma camada de rede, um parser ou um motor de dados não tem motivo para padronizar para o main actor, e fazê-lo força @concurrent ou nonisolated em quase tudo. Deixe SWIFT_DEFAULT_ACTOR_ISOLATION sem definição para esses alvos e isole de forma deliberada, da maneira antiga.
  • Um sistema concorrente pesado em actors. Se o seu design genuinamente roda muitas coisas em paralelo (um pipeline de verdade, não um app com algumas tarefas em segundo plano), o padrão de main actor luta contra você. Você quer actors explícitos e código nonisolated, e o padrão da SE-0466 é o ponto de partida errado.

Para um app, porém, a decisão é fácil: ative ambas as configurações, deixe a contagem de erros desabar e trate o punhado que resta como um mapa de exatamente onde seu código sai da thread principal. Vale a pena ter esse mapa. O modelo antigo lhe dava mil avisos e nenhum mapa; o novo lhe dá seis fronteiras honestas e um padrão que finalmente corresponde a como o app roda.

Uma última observação sobre ruído versus sinal. O SourceKit vai mostrar erros de índice entre arquivos (“Cannot find type X in scope”, “No such module”) dentro do editor enquanto o Xcode reconstrói seu índice, especialmente logo depois de regenerar um projeto. Esses são artefatos de índice, não erros de concorrência. Se o xcodebuild reportar BUILD SUCCEEDED, o modelo de concorrência está satisfeito e o editor está apenas se atualizando1. Perseguir fantasmas de índice é a maneira mais rápida de desperdiçar uma tarde em uma migração que já funcionava.

FAQ

Como habilito a concorrência main-actor-por-padrão no Swift 6.2?

Defina duas build settings, ambas ativadas por padrão em novos projetos do Xcode 26: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor e SWIFT_APPROACHABLE_CONCURRENCY = YES5. Em um Swift package você define as flags de upcoming-feature equivalentes através de swiftSettings em vez da build setting do Xcode6.

O que SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor realmente muda?

Faz o código de um módulo ser main-actor-isolated por padrão (SE-0466), então uma view, seu model e seus helpers são todos @MainActor sem anotações2. A concorrência passa a ser a exceção nomeada que você busca em vez da condição ambiente contra a qual você anota em cada linha.

O que é @concurrent e quando devo usá-lo?

@concurrent transfere uma função async para o executor global, de propósito, para longe do actor do seu chamador4. Sob um padrão de main actor você o usa para o trabalho que comprovadamente precisa de uma thread em segundo plano (decodificar um payload grande, redimensionar uma imagem); é mutuamente exclusivo com @MainActor, então uma função ou roda onde seu chamador está ou roda deliberadamente para longe dele.

Por que recebo “Main actor-isolated … can not be referenced from a nonisolated context”?

Sob o padrão de main actor, funções puras, statics singleton e constantes herdam o isolamento de main actor, e então dão erro quando chamadas de um contexto nonisolated, como a avaliação de um parâmetro padrão. A correção é uma anotação nonisolated precisa no membro, não uma supressão; o valor é seguro para compartilhar porque não toca em estado de actor nenhum7.

Devo usar main-actor-por-padrão para um alvo de biblioteca ou framework?

Não. Uma camada de rede, parser ou motor de dados não tem motivo para padronizar para o main actor, e fazê-lo força @concurrent ou nonisolated em quase tudo. Deixe SWIFT_DEFAULT_ACTOR_ISOLATION sem definição para alvos sem UI e isole de forma deliberada, da maneira antiga.

Fazer await em uma função async nonisolated ainda salta actors no Swift 6.2?

Não. A SE-0461 faz uma função async nonisolated rodar em qualquer actor que a chamou em vez de saltar para o executor global, então fazer await nela não arrasta mais você através de uma fronteira de isolamento nem exige Sendable em tudo que está em escopo3.



  1. Código de produção do autor nos apps iOS da família 941 (Ki, Return, Get Bananas), todos publicados com SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor e SWIFT_APPROACHABLE_CONCURRENCY = YES. Os seis padrões de erro e correções abaixo foram o conjunto completo de limpeza de concorrência estrita do Ki 1.0.0. 

  2. Swift Evolution, SE-0466: Control default actor isolation inference. Permite que um módulo tenha o isolamento @MainActor como padrão, então alvos de UI e de app rodam no main actor a menos que o código opte por sair via @concurrent ou um actor explícito. 

  3. Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. Uma função async nonisolated assume nonisolated(nonsending) por padrão, rodando no actor do chamador em vez de saltar para o executor global, o que remove os requisitos de Sendable na travessia de fronteira que vinham com o salto. 

  4. Swift Evolution, SE-0461 introduz o atributo @concurrent para fazer uma função optar por rodar no executor global (uma thread em segundo plano). @concurrent e nonisolated(nonsending) são os dois modos de isolamento opostos para uma função async nonisolated: uma função ou roda onde seu chamador está ou roda deliberadamente para longe dele. @concurrent não pode ser combinado com @MainActor ou um global actor customizado. 

  5. SWIFT_APPROACHABLE_CONCURRENCY é a build setting guarda-chuva do Xcode que habilita os upcoming features de approachable-concurrency (incluindo o comportamento da SE-0461), e SWIFT_DEFAULT_ACTOR_ISOLATION seleciona o isolamento padrão do módulo. Novos projetos do Xcode 26 habilitam ambos com o padrão de main actor. Documentado em Donny Wals, “Exploring concurrency changes in Swift 6.2”, e Paul Hudson, “What’s new in Swift 6.2”, ambos cruzados com as propostas subjacentes SE-0461 e SE-0466. 

  6. Swift, Swift Concurrency Migration Guide, “Enabling Complete Concurrency Checking” e configuração de language-mode. Em um Swift package, o isolamento padrão e os recursos de approachable-concurrency são definidos através das flags de upcoming-feature de swiftSettings em vez da build setting do Xcode. 

  7. Swift, Migration Guide: global actor isolation and nonisolated. Os métodos de um tipo @MainActor herdam o isolamento de main actor; nonisolated faz um método optar por sair, o que é correto para funções puras que não tocam em estado isolado nenhum. 

  8. Apple Developer, MainActor.assumeIsolated(_:). Afirma que a execução atual já está no main actor e roda a closure de forma síncrona sem um salto de actor. A afirmação dispara um trap em runtime se a invariante não se sustentar, então só é válida onde o chamador tem garantia de estar na thread principal. 

  9. Apple, WWDC 2026 Swift Group Lab. Comentários atribuídos a Holly e Doug da equipe central do Swift são paráfrases de uma gravação transcrita localmente; a Apple não publicou transcrição ou legendas oficiais para a sessão, então a redação é aproximada, não uma citação literal. 

Artigos relacionados

O que o time de Swift da Apple disse no Lab da WWDC26

O Swift Group Lab da Apple na WWDC26 saiu sem legendas. Nós o transcrevemos localmente. Respostas francas dos engenheiro…

13 min de leitura

O que há de novo no Swift (2026): a atualização da WWDC26

Swift 6.3 e 6.4 da WWDC26: disponibilidade anyAppleOS, seletores de módulo, acessadores borrow/mutate, o protocolo Itera…

19 min de leitura

Dois servidores MCP transformaram Claude Code em um sistema de build iOS

XcodeBuildMCP e o Xcode MCP da Apple dão ao Claude Code acesso estruturado a builds, testes e debugging no iOS. Setup, r…

17 min de leitura