tva
← Insights

Concorrência no Swift 6: Um Guia Prático de Migração

O Swift 5 era tolerante com relação a corridas de dados. O compilador avisava sobre algumas delas e ignorava outras, o que fazia com que as bases de código desenvolvessem hábitos – chamando DispatchQueue.main.async em alguns lugares, esquecendo em outros – que funcionavam até que paravam. O Swift 6 muda o contrato: todos os avisos sobre isolamento de atores e conformidade com Sendable se tornam erros rígidos, e o build se recusa a prosseguir até que o potencial de corrida de dados seja eliminado ou explicitamente reconhecido.

Na realidade, a maioria dos aplicativos SwiftUI existentes não tem corridas de dados catastróficas escondidas neles. O que eles têm é uma coleção de padrões que eram seguros na prática, mas tecnicamente incorretos de acordo com o modelo de concorrência do Swift. A migração tem menos a ver com corrigir bugs reais e mais com tornar as propriedades de segurança do código legíveis para o compilador – o que é um exercício valioso, mas requer entender o que os erros estão realmente dizendo.

Habilitando a Verificação Estrita de Concorrência

Antes de fazer upgrade para o modo de linguagem Swift 6, é útil visualizar os erros no nível de aviso. No Xcode, defina a configuração do compilador Swift SWIFT_STRICT_CONCURRENCY como complete para seu alvo. Isso mostra tudo que o Swift 6 rejeitaria, mas não falha o build. Passe pelos avisos sistematicamente antes de mudar a versão da linguagem.

Para habilitar o modo de linguagem Swift 6 nas Configurações de Build do seu alvo, defina SWIFT_VERSION como 6. Uma vez definido, a verificação estrita de concorrência complete está implícita e todos os avisos se tornam erros. O upgrade é melhor feito alvo por alvo em um projeto com múltiplos pacotes, começando pelos pacotes com menos dependências de UI – pacotes de lógica pura e de rede primeiro, pacotes de UI por último.

O Problema da Fronteira do MainActor

A classe de erros mais comum na migração SwiftUI parece com isso:

// Error: Main actor-isolated property 'items' can not be
// mutated from a nonisolated context
class DataStore: ObservableObject {
    @Published var items: [Item] = []

    func fetch() {
        Task {
            let result = await API.load()
            self.items = result  // ← error
        }
    }
}

As propriedades @Published em um ObservableObject não são automaticamente isoladas em @MainActor, mas o SwiftUI as observa na thread principal. Quando um Task {} roda em um executor não especificado e tenta escrever em items, o Swift 6 detecta a potencial corrida.

A correção mais limpa é anotar a classe inteira com @MainActor:

@MainActor
class DataStore: ObservableObject {
    @Published var items: [Item] = []

    func fetch() async {
        let result = await API.load()
        items = result  // ✓ — a classe inteira é isolada no MainActor
    }
}

Isso torna o isolamento da classe explícito. Chamadores que invocam fetch() em um contexto de segundo plano precisarão usar await, o que é correto – eles devem estar no main actor para interagir com uma classe que alimenta atualizações de UI. Se você não puder anotar a classe completa, a alternativa é saltar explicitamente:

func fetch() {
    Task {
        let result = await API.load()
        await MainActor.run {
            self.items = result
        }
    }
}

Ambas as abordagens são válidas. A anotação de classe @MainActor é mais limpa quando o propósito principal da classe é alimentar views SwiftUI – que é quase sempre o caso para classes ObservableObject em um aplicativo bem estruturado.

PhotosPicker e Isolamento de Delegate

O PHPickerViewControllerDelegate é uma fonte comum de erros do Swift 6 porque seu callback chega em uma thread de segundo plano, mas o código no callback normalmente precisa atualizar a UI. O padrão clássico do Swift 5:

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    dismiss(animated: true)
    guard let provider = results.first?.itemProvider else { return }

    provider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
        if let image = object as? UIImage {
            self?.imageView.image = image  // ← erro do Swift 6
        }
    }
}

No Swift 6, a closure passada para loadObject é @Sendable, o que significa que não pode capturar self se self não for Sendable. E imageView.image é uma propriedade isolada em @MainActor sendo acessada a partir de um contexto de segundo plano.

A correção é saltar para o main actor dentro da closure:

provider.loadObject(ofClass: UIImage.self) { [weak self] object, error in
    guard let image = object as? UIImage else { return }
    Task { @MainActor [weak self] in
        self?.imageView.image = image
    }
}

A sintaxe Task { @MainActor in } cria uma task que está explicitamente isolada no main actor. A captura de image está correta porque UIImage está em conformidade com Sendable. A captura de self via [weak self] está correta porque é uma referência opcional fraca, não uma transferência de estado mutável compartilhado.

Para o PhotosPicker do SwiftUI (a API mais recente), o padrão é mais limpo porque usa Transferable:

// Em um view model @MainActor:
func handlePickerSelection(_ item: PhotosPickerItem?) async {
    guard let item else { return }
    if let data = try? await item.loadTransferable(type: Data.self) {
        selectedImage = UIImage(data: data)
    }
}

Como a chamada loadTransferable é async, você a usa com await, e a atribuição ocorre em qualquer ator em que a função está isolada – que é @MainActor se o view model tiver essa anotação. Nenhum salto manual de ator necessário.

Tipos Sendable e Não-Sendable

Um tipo é Sendable se é seguro transferi-lo através de fronteiras de isolamento de atores – grosso modo, se é um tipo de valor, ou uma classe que protege seu estado através de sincronização. O compilador impõe isso no Swift 6 sempre que um valor atravessa uma fronteira de isolamento.

O padrão de erro que aparece com mais frequência:

// Error: Capture of 'config' with non-sendable type 'RequestConfig'
// in a '@Sendable' closure
struct RequestConfig {
    var endpoint: String
    var headers: [String: String]
    var body: Data?
}

Task {
    let result = await API.fetch(config)  // config capturado aqui
}

A correção geralmente é direta – adicionar conformidade Sendable ao tipo de valor:

struct RequestConfig: Sendable {
    var endpoint: String
    var headers: [String: String]
    var body: Data?
}

Para structs cujos membros são todos Sendable (o que inclui String, Data e todos os tipos de valor padrão), essa conformidade é trivialmente satisfeita e o compilador a sintetizará. Para classes, a história é mais complexa: uma classe é Sendable apenas se for imutável (final com todas as propriedades let) ou sincronizada internamente. Uma classe ObservableObject pode ser marcada como @unchecked Sendable se você souber que sua sincronização está sendo tratada – tipicamente porque é sempre acessada no main actor – mas use isso como último recurso, não como primeira resposta.

SharedDataProvider e Estado Entre View Models

Um padrão comum em aplicativos SwiftUI maiores é um provedor de dados compartilhado que é injetado em múltiplas views via o ambiente. No Swift 6, esse objeto precisa de isolamento de ator explícito porque é acessado de corpos de view, que rodam no main actor:

@MainActor
class SharedDataProvider: ObservableObject {
    static let shared = SharedDataProvider()

    @Published var currentUser: User?
    @Published var sessionToken: String?

    func refreshSession() async {
        let token = await AuthClient.refresh()
        sessionToken = token
    }
}

A anotação @MainActor aqui está fazendo trabalho real: garante que todo acesso a currentUser e sessionToken seja serializado pelo main actor, que é o isolamento correto para propriedades que alimentam atualizações de view SwiftUI. Tasks em segundo plano que precisam escrever nessa classe devem usar await, o que é correto – elas devem esperar que o main actor esteja disponível em vez de escrever concorrentemente.

A sutileza é que static let shared em uma classe @MainActor só pode ser acessada no main actor. Se você tiver qualquer caminho de código que tente acessar SharedDataProvider.shared de um contexto de segundo plano, o Swift 6 o rejeitará. A correção é ou passar a instância explicitamente pela cadeia de funções, ou envolver o acesso em await MainActor.run { ... }.

A Saída de Emergência @unchecked Sendable

Algumas classes não podem ser tornadas completamente Sendable na prática – elas podem envolver bibliotecas C, criar pontes para tipos Objective-C, ou ter estado interno gerenciado com segurança, mas de forma opaca. Para esses casos, @unchecked Sendable diz ao compilador “confie em mim, cuidei da sincronização”:

final class AudioPlayer: @unchecked Sendable {
    private let queue = DispatchQueue(label: "audio.player")
    private var _isPlaying = false

    var isPlaying: Bool {
        queue.sync { _isPlaying }
    }

    func play() {
        queue.async { self._isPlaying = true }
    }
}

Isso não é um atalho para evitar o trabalho de migração. É uma declaração explícita de que você pensou sobre a semântica de concorrência e está assumindo a responsabilidade por ela. Use para classes que genuinamente precisam de sincronização interna, e adicione um comentário explicando qual mecanismo fornece a garantia de segurança.

Uma Ordem Prática de Migração

Habilite SWIFT_STRICT_CONCURRENCY = complete primeiro e trate todos os avisos antes de mudar para o modo de linguagem Swift 6. Comece pelos pacotes folha no seu grafo de dependências – os que não têm dependências em outros módulos internos. Corrija-os, atualize-os para o modo de linguagem Swift 6, depois passe para a próxima camada. Isso evita uma situação em que corrigir um pacote downstream requer mudanças simultâneas em um pacote upstream que ainda está em modo de aviso.

A maior parte do trabalho de migração em uma base de código SwiftUI típica são anotações @MainActor em classes ObservableObject, conformidade Sendable em tipos de valor passados através de fronteiras de task, e saltos explícitos de ator em callbacks de delegate. O volume de mudanças parece grande em um diff, mas cada mudança individual é mecânica. O compilador guia você para cada local que precisa de atenção – que é o ponto da verificação estrita de concorrência.

Insights Relacionados

Artigos relacionados