Pra Caralho Acessível
Swift Concurrency
Finalmente entenda async/await, actors e Sendable. Modelos mentais claros, sem jargão.
Enorme agradecimento a Matt Massicotte por tornar a concorrência em Swift compreensível. Compilado por Pedro Piñera. Encontrou um erro? [email protected]
Na tradição de fuckingblocksyntax.com e fuckingifcaseletsyntax.com
Escale seu desenvolvimento com Tuist
A Verdade Nua e Crua
Não existe cola para concorrência em Swift. Cada resposta "apenas faça X" está errada em algum contexto.
Mas aqui está a boa notícia: Uma vez que você entenda o isolamento (5 min de leitura), tudo faz sentido. Os erros do compilador começam a fazer sentido. Você para de lutar contra o sistema e começa a trabalhar com ele.
Este guia é voltado para Swift 6+. A maioria dos conceitos se aplica ao Swift 5.5+, mas o Swift 6 aplica verificação de concorrência mais rigorosa.
A Única Coisa Que Você Precisa Entender
Isolamento é a chave para tudo. É a resposta do Swift à pergunta: Quem tem permissão para tocar nesses dados agora?
O Prédio de Escritórios
Pense no seu app como um prédio de escritórios. Cada escritório é um domínio de isolamento - um espaço privado onde apenas uma pessoa pode trabalhar por vez. Você não pode simplesmente invadir o escritório de outra pessoa e começar a reorganizar a mesa dela.
Vamos construir sobre essa analogia ao longo do guia.
Por Que Não Apenas Threads?
Por décadas, escrevemos código concorrente pensando em threads. O problema? Threads não te impedem de dar um tiro no pé. Duas threads podem acessar os mesmos dados simultaneamente, causando data races - bugs que crasham aleatoriamente e são quase impossíveis de reproduzir.
Em um telefone, você pode escapar. Em um servidor lidando com milhares de requisições concorrentes, data races se tornam uma certeza - geralmente aparecendo em produção, numa sexta-feira. À medida que o Swift se expande para servidores e outros ambientes altamente concorrentes, "esperar o melhor" não funciona.
A abordagem antiga era defensiva: usar locks, dispatch queues, esperar não ter esquecido nada.
A abordagem do Swift é diferente: tornar data races impossíveis em tempo de compilação. Em vez de perguntar "em qual thread isso está?", o Swift pergunta "quem tem permissão para tocar nesses dados agora?" Isso é isolamento.
Como Outras Linguagens Lidam Com Isso
| Linguagem | Abordagem | Quando você descobre os bugs |
|---|---|---|
| Swift | Isolamento + Sendable | Tempo de compilação |
| Rust | Ownership + borrow checker | Tempo de compilação |
| Go | Channels + detector de races | Runtime (com ferramentas) |
| Java/Kotlin | synchronized, locks |
Runtime (crashes) |
| JavaScript | Event loop single-threaded | Evitado completamente |
| C/C++ | Locks manuais | Runtime (comportamento indefinido) |
Swift e Rust são as únicas linguagens mainstream que detectam data races em tempo de compilação. O trade-off? Uma curva de aprendizado mais íngreme no início. Mas uma vez que você entende o modelo, o compilador te protege.
Aqueles erros chatos sobre Sendable e isolamento de actor? Eles estão detectando bugs que antes seriam crashes silenciosos.
Os Domínios de Isolamento
Agora que você entende isolamento (escritórios privados), vamos olhar os diferentes tipos de escritórios no prédio do Swift.
O Prédio de Escritórios
- A recepção (
MainActor) - onde todas as interações com clientes acontecem. Só existe uma, e ela lida com tudo que o usuário vê. - Escritórios de departamento (
actor) - contabilidade, jurídico, RH. Cada departamento tem seu próprio escritório protegendo seus próprios dados sensíveis. - Corredores e áreas comuns (
nonisolated) - espaços compartilhados por onde qualquer um pode passar. Sem dados privados aqui.
MainActor: A Recepção
O MainActor é um domínio de isolamento especial que roda na thread principal. É onde todo o trabalho de UI acontece.
@MainActor
@Observable
class ViewModel {
var items: [Item] = [] // Estado da UI vive aqui
func refresh() async {
let newItems = await fetchItems()
self.items = newItems // Seguro - estamos no MainActor
}
}
Na dúvida, use MainActor
Para a maioria dos apps, marcar seus ViewModels e classes relacionadas a UI com @MainActor é a escolha certa. Preocupações com performance geralmente são exageradas - comece aqui, otimize apenas se você medir problemas reais.
Actors: Escritórios de Departamento
Um actor é como um escritório de departamento - ele protege seus próprios dados e permite apenas um visitante por vez.
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount // Seguro! Apenas um chamador por vez
}
}
Sem actors, duas threads leem balance = 100, ambas somam 50, ambas escrevem 150 - você perdeu $50. Com actors, o Swift automaticamente enfileira o acesso e ambos os depósitos completam corretamente.
Não abuse dos actors
Você precisa de um actor personalizado apenas quando todas as quatro condições são verdadeiras:
- Você tem estado mutável não-Sendable (não thread-safe)
- Múltiplos lugares precisam acessar
- Operações nesse estado devem ser atômicas
- Não pode simplesmente viver no MainActor
Se alguma condição for falsa, você provavelmente não precisa de um actor. A maioria do estado de UI pode viver no @MainActor. Leia mais sobre quando usar actors.
Nonisolated: Os Corredores
Código marcado como nonisolated é como os corredores - não pertence a nenhum escritório e pode ser acessado de qualquer lugar.
actor UserSession {
let userId: String // Imutável - seguro ler de qualquer lugar
var lastActivity: Date // Mutável - precisa proteção do actor
nonisolated var displayId: String {
"User: \(userId)" // Só lê dados imutáveis
}
}
// Uso - não precisa de await para nonisolated
let session = UserSession(userId: "123")
print(session.displayId) // Funciona sincronamente!
Use nonisolated para propriedades computadas que só leem dados imutáveis.
Como o Isolamento Se Propaga
Quando você marca um tipo com um isolamento de actor, o que acontece com seus métodos? E os closures? Entender como o isolamento se propaga é a chave para evitar surpresas.
O Prédio de Escritórios
Quando você é contratado em um departamento, você trabalha no escritório desse departamento por padrão. Se o departamento de Marketing te contrata, você não aparece aleatoriamente na Contabilidade.
Da mesma forma, quando uma função é definida dentro de uma classe @MainActor, ela herda esse isolamento. Ela "trabalha no mesmo escritório" que seu pai.
Classes Herdam Seu Isolamento
@MainActor
class ViewModel {
var count = 0 // Isolado no MainActor
func increment() { // Também isolado no MainActor
count += 1
}
}
Tudo dentro da classe herda @MainActor. Você não precisa marcar cada método.
Tasks Herdam o Contexto (Geralmente)
@MainActor
class ViewModel {
func doWork() {
Task {
// Isso herda MainActor!
self.updateUI() // Seguro, nao precisa de await
}
}
}
Um Task { } criado de um contexto @MainActor fica no MainActor. Isso geralmente é o que você quer.
Task.detached Quebra a Herança
@MainActor
class ViewModel {
func doWork() {
Task.detached {
// NAO esta mais no MainActor!
await self.updateUI() // Agora precisa de await
}
}
}
O Prédio de Escritórios
Task.detached é como contratar um freelancer externo. Ele não tem crachá para seu escritório - ele trabalha do próprio espaço e precisa passar pelos canais apropriados para acessar suas coisas.
Task.detached geralmente está errado
Na maioria das vezes, você quer um Task regular. Tasks detached não herdam prioridade, valores task-local, ou contexto de actor. Use-os apenas quando você explicitamente precisar dessa separação.
O Que Pode Cruzar Fronteiras
Agora que você sabe sobre domínios de isolamento (escritórios) e como eles se propagam, a próxima pergunta é: o que você pode passar entre eles?
O Prédio de Escritórios
Nem tudo pode sair de um escritório:
- Fotocópias são seguras de compartilhar - se o Jurídico faz uma cópia de um documento e envia para a Contabilidade, ambos têm sua própria cópia. Sem conflito.
- Contratos originais assinados devem ficar no lugar - se dois departamentos pudessem modificar o original, o caos acontece.
Em termos de Swift: tipos Sendable são fotocópias (seguras de compartilhar), tipos não-Sendable são originais (devem ficar em um escritório).
Sendable: Seguro para Compartilhar
Esses tipos podem cruzar fronteiras de isolamento com segurança:
// Structs com dados imutáveis - como fotocópias
struct User: Sendable {
let id: Int
let name: String
}
// Actors se protegem - eles lidam com seus próprios visitantes
actor BankAccount { } // Automaticamente Sendable
Automaticamente Sendable:
- Tipos de valor (structs, enums) com propriedades Sendable
- Actors (eles se protegem)
- Classes imutáveis (
final classcom apenas propriedadeslet)
Não-Sendable: Devem Ficar
Esses tipos não podem cruzar fronteiras com segurança:
// Classes com estado mutável - como documentos originais
class Counter {
var count = 0 // Dois escritórios modificando isso = desastre
}
Por que essa é a distinção chave? Porque todo erro de compilador que você encontrar se resume a: "Você está tentando enviar um tipo não-Sendable através de uma fronteira de isolamento."
Quando o Compilador Reclama
Se o Swift diz que algo não é Sendable, você tem opções:
- Faça um tipo de valor - use
structem vez declass - Isole - mantenha no
@MainActorpara que não precise cruzar - Mantenha não-Sendable - apenas não passe entre escritórios
- Último recurso:
@unchecked Sendable- você promete que é seguro (cuidado)
Comece não-Sendable
Matt Massicotte defende começar com tipos regulares, não-Sendable. Adicione Sendable apenas quando precisar cruzar fronteiras. Um tipo não-Sendable permanece simples e evita dores de cabeça de conformance.
Como Cruzar Fronteiras
Você entende domínios de isolamento, você sabe o que pode cruzá-los. Agora: como você realmente se comunica entre escritórios?
O Prédio de Escritórios
Você não pode simplesmente invadir outro escritório. Você envia uma requisição e espera uma resposta. Você pode trabalhar em outras coisas enquanto espera, mas você precisa dessa resposta antes de poder continuar.
Isso é async/await - enviar uma requisição para outro domínio de isolamento e pausar até obter uma resposta.
A Palavra-Chave await
Quando você chama uma função em outro actor, você precisa de await:
actor DataStore {
var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
}
@MainActor
class ViewModel {
let store = DataStore()
func addItem(_ item: Item) async {
await store.add(item) // Requisição para outro escritório
updateUI() // De volta ao nosso escritório
}
}
O await significa: "Envie essa requisição e pause até terminar. Posso fazer outro trabalho enquanto espero."
Suspensão, Não Bloqueio
Conceito Errôneo Comum
Muitos desenvolvedores assumem que adicionar async faz o código rodar em segundo plano. Não faz. A palavra-chave async apenas significa que a função pode pausar. Não diz nada sobre onde ela roda.
A chave é a diferença entre bloqueio e suspensão:
- Bloqueio: Você senta na sala de espera olhando para a parede. Nada mais acontece.
- Suspensão: Você deixa seu telefone e faz outras coisas. Eles ligam quando estiver pronto.
// Thread fica ociosa, sem fazer nada por 5 segundos
Thread.sleep(forTimeInterval: 5)
// Thread está livre para fazer outro trabalho enquanto espera
try await Task.sleep(for: .seconds(5))
Iniciando Trabalho Async de Código Síncrono
Às vezes você está em código síncrono e precisa chamar algo async. Use Task:
@MainActor
class ViewModel {
func buttonTapped() { // Função síncrona
Task {
await loadData() // Agora podemos usar await
}
}
}
O Prédio de Escritórios
Task é como atribuir trabalho a um funcionário. O funcionário lida com a requisição (incluindo esperar outros escritórios) enquanto você continua com seu trabalho imediato.
Padrões Que Funcionam
O Padrão de Requisição de Rede
@MainActor
@Observable
class ViewModel {
var users: [User] = []
var isLoading = false
func fetchUsers() async {
isLoading = true
// Isso suspende - thread está livre para fazer outro trabalho
let users = await networkService.getUsers()
// De volta no MainActor automaticamente
self.users = users
isLoading = false
}
}
Sem DispatchQueue.main.async. O atributo @MainActor cuida disso.
Trabalho Paralelo com async let
func loadProfile() async -> Profile {
async let avatar = loadImage("avatar.jpg")
async let banner = loadImage("banner.jpg")
async let details = loadUserDetails()
// Os tres rodam em paralelo!
return Profile(
avatar: await avatar,
banner: await banner,
details: await details
)
}
Prevenindo Toques Duplos
Esse padrão vem do guia de Matt Massicotte sobre sistemas com estado:
@MainActor
class ButtonViewModel {
private var isLoading = false
func buttonTapped() {
// Guard SINCRONAMENTE antes de qualquer trabalho async
guard !isLoading else { return }
isLoading = true
Task {
await doExpensiveWork()
isLoading = false
}
}
}
Crítico: O guard deve ser síncrono
Se você colocar o guard dentro do Task depois de um await, há uma janela onde dois toques de botão podem ambos iniciar trabalho. Aprenda mais sobre ordenação e concorrência.
Erros Comuns a Evitar
Esses são erros comuns que até desenvolvedores experientes cometem:
Pensar que async = segundo plano
O Prédio de Escritórios
Adicionar async não te move para um escritório diferente. Você ainda está na recepção - você só pode esperar entregas agora sem congelar no lugar.
// Isso AINDA bloqueia a thread principal!
@MainActor
func slowFunction() async {
let result = expensiveCalculation() // Síncrono = bloqueante
data = result
}
Se você precisa de trabalho feito em outro escritório, envie explicitamente para lá:
func slowFunction() async {
let result = await Task.detached {
expensiveCalculation() // Agora em um escritório diferente
}.value
await MainActor.run { data = result }
}
Criar muitos actors
O Prédio de Escritórios
Criar um novo escritório para cada pedaço de dados significa papelada interminável para comunicar entre eles. A maioria do seu trabalho pode acontecer na recepção.
// Sobre-engenheirado - cada chamada requer caminhar entre escritórios
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }
// Melhor - a maioria das coisas pode viver na recepção
@MainActor
class AppState { }
Usar MainActor.run em todo lugar
O Prédio de Escritórios
Se você continua caminhando até a recepção para cada coisinha, apenas trabalhe lá. Faça parte da sua descrição de trabalho, não uma tarefa constante.
// Não faça isso - caminhando constantemente até a recepção
await MainActor.run { doMainActorStuff() }
// Faça isso - apenas trabalhe na recepção
@MainActor func doMainActorStuff() { }
Fazer tudo Sendable
Nem tudo precisa ser Sendable. Se você está adicionando @unchecked Sendable em todo lugar, você está fazendo fotocópias de coisas que não precisam sair do escritório.
Ignorar warnings do compilador
Cada warning do compilador sobre Sendable é o guarda de segurança te dizendo que algo não é seguro para carregar entre escritórios. Não ignore - entenda.
Erros Comuns do Compilador
Essas são as mensagens de erro reais que você verá. Cada uma é o compilador te protegendo de um data race.
"Sending 'self.foo' risks causing data races"
O Prédio de Escritórios
Você está tentando carregar um documento original para outro escritório. Ou faça uma fotocópia (Sendable) ou mantenha em um lugar.
Solução 1: Use uma struct em vez de uma class
Solução 2: Mantenha em um actor:
@MainActor
class MyClass {
var foo: SomeType // Fica na recepção
}
"Non-sendable type cannot cross actor boundary"
O Prédio de Escritórios
Você está tentando carregar um original entre escritórios. O guarda de segurança te parou.
Solução 1: Faça uma struct:
// Antes: class (não-Sendable)
class User { var name: String }
// Depois: struct (Sendable)
struct User: Sendable { let name: String }
Solução 2: Isole em um actor:
@MainActor
class User { var name: String }
"Actor-isolated property cannot be referenced"
O Prédio de Escritórios
Você está metendo a mão no arquivo de outro escritório sem passar pelos canais apropriados.
Solução: Use await:
// Errado - metendo a mão diretamente
let value = myActor.balance
// Certo - requisição apropriada
let value = await myActor.balance
"Call to main actor-isolated method in synchronous context"
O Prédio de Escritórios
Você está tentando usar a recepção sem esperar na fila.
Solução 1: Faça o chamador @MainActor:
@MainActor
func doSomething() {
updateUI() // Mesmo isolamento, não precisa de await
}
Solução 2: Use await:
func doSomething() async {
await updateUI()
}
Três Níveis de Swift Concurrency
Você não precisa aprender tudo de uma vez. Progrida através desses níveis:
O Prédio de Escritórios
Pense nisso como crescer uma empresa. Você não começa com uma sede de 50 andares - você começa com uma mesa.
Esses níveis não são limites rígidos - diferentes partes do seu app podem precisar de níveis diferentes. Um app principalmente-Nível-1 pode ter uma feature que precisa de padrões de Nível 2. Tudo bem. Use a abordagem mais simples que funcione para cada parte.
Nível 1: A Startup
Todos trabalham na recepção. Simples, direto, sem burocracia.
- Use
async/awaitpara chamadas de rede - Marque classes de UI com
@MainActor - Use o modificador
.taskdo SwiftUI
Isso cobre 80% dos apps. Apps como Things, Bear, Flighty, ou Day One provavelmente caem nessa categoria - apps que principalmente buscam dados e os exibem.
Nível 2: A Empresa em Crescimento
Você precisa lidar com múltiplas coisas de uma vez. Hora de projetos paralelos e coordenar equipes.
- Use
async letpara trabalho paralelo - Use
TaskGrouppara paralelismo dinâmico - Entenda cancelamento de tasks
Apps como Ivory/Ice Cubes (clientes Mastodon gerenciando múltiplas timelines e atualizações em streaming), Overcast (coordenando downloads, reprodução e sincronização em segundo plano), ou Slack (mensagens em tempo real através de múltiplos canais) podem usar esses padrões para certas features.
Nível 3: A Corporação
Departamentos dedicados com suas próprias políticas. Comunicação inter-escritório complexa.
- Crie actors personalizados para estado compartilhado
- Entendimento profundo de Sendable
- Executors personalizados
Apps como Xcode, Final Cut Pro, ou frameworks Swift server-side como Vapor e Hummingbird provavelmente precisam desses padrões - estado compartilhado complexo, milhares de conexões concorrentes, ou código nível-framework sobre o qual outros constroem.
Comece simples
A maioria dos apps nunca precisa do Nível 3. Não construa uma corporação quando uma startup é suficiente.
Glossário: Mais Palavras-Chave Que Você Encontrará
Além dos conceitos básicos, aqui estão outras palavras-chave de concorrência do Swift que você verá no mundo real:
| Palavra-chave | O que significa |
|---|---|
nonisolated |
Opta por sair do isolamento de um actor - roda sem proteção |
isolated |
Declara explicitamente que um parâmetro roda no contexto de um actor |
@Sendable |
Marca um closure como seguro para passar através de fronteiras de isolamento |
Task.detached |
Cria um task completamente separado do contexto atual |
AsyncSequence |
Uma sequência que você pode iterar com for await |
AsyncStream |
Uma forma de conectar código baseado em callbacks com sequências async |
withCheckedContinuation |
Conecta completion handlers com async/await |
Task.isCancelled |
Verifica se o task atual foi cancelado |
@preconcurrency |
Suprime warnings de concorrência para código legado |
GlobalActor |
Protocolo para criar seus próprios actors personalizados como MainActor |
Quando Usar Cada Um
nonisolated - Ler propriedades computadas
Por padrão, tudo dentro de um actor está isolado - você precisa de await para acessar. Mas às vezes você tem propriedades que são inerentemente seguras de ler: constantes let imutáveis, ou propriedades computadas que só derivam valores de outros dados seguros. Marcar essas como nonisolated permite que chamadores as acessem sincronamente, evitando overhead async desnecessário.
actor UserSession {
let userId: String // Imutável, seguro de ler
var lastActivity: Date // Mutável, precisa proteção
// Isso pode ser chamado sem await
nonisolated var displayId: String {
"User: \(userId)" // Só lê dados imutáveis
}
}
// Uso
let session = UserSession(userId: "123")
print(session.displayId) // Não precisa de await!
@Sendable - Closures que cruzam fronteiras
Quando um closure escapa para rodar mais tarde ou em um domínio de isolamento diferente, o Swift precisa garantir que não causará data races. O atributo @Sendable marca closures que são seguros de passar através de fronteiras - eles não podem capturar estado mutável de forma insegura. O Swift frequentemente infere isso automaticamente (como com Task.detached), mas às vezes você precisa declarar explicitamente ao projetar APIs que aceitam closures.
@MainActor
class ViewModel {
var items: [Item] = []
func processInBackground() {
Task.detached {
// Esse closure cruza do task detached para o MainActor
// Deve ser @Sendable (Swift infere isso)
let processed = await self.heavyProcessing()
await MainActor.run {
self.items = processed
}
}
}
}
// @Sendable explícito quando necessário
func runLater(_ work: @Sendable @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
work()
}
}
withCheckedContinuation - Conectando APIs antigas
Muitas APIs antigas usam completion handlers em vez de async/await. Em vez de reescrevê-las completamente, você pode envolvê-las usando withCheckedContinuation. Essa função suspende o task atual, te dá um objeto continuation, e retoma quando você chama continuation.resume(). A variante "checked" detecta erros de programação como resumir duas vezes ou nunca resumir.
// API antiga baseada em callbacks
func fetchUser(id: String, completion: @escaping (User?) -> Void) {
// ... chamada de rede com callback
}
// Envolvida como async
func fetchUser(id: String) async -> User? {
await withCheckedContinuation { continuation in
fetchUser(id: id) { user in
continuation.resume(returning: user) // Conecta de volta!
}
}
}
Para funções que lançam, use withCheckedThrowingContinuation:
func fetchUserThrowing(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
AsyncStream - Conectando fontes de eventos
Enquanto withCheckedContinuation lida com callbacks únicos, muitas APIs entregam múltiplos valores ao longo do tempo - métodos delegate, NotificationCenter, ou sistemas de eventos personalizados. AsyncStream conecta esses ao AsyncSequence do Swift, permitindo que você use loops for await. Você cria um stream, guarda sua continuation, e chama yield() cada vez que um novo valor chega.
class LocationTracker: NSObject, CLLocationManagerDelegate {
private var continuation: AsyncStream<CLLocation>.Continuation?
var locations: AsyncStream<CLLocation> {
AsyncStream { continuation in
self.continuation = continuation
}
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
for location in locations {
continuation?.yield(location)
}
}
}
// Uso
let tracker = LocationTracker()
for await location in tracker.locations {
print("Nova localização: \(location)")
}
Task.isCancelled - Cancelamento cooperativo
Swift usa cancelamento cooperativo - quando um task é cancelado, ele não para imediatamente. Em vez disso, uma flag é definida, e é sua responsabilidade verificá-la periodicamente. Isso te dá controle sobre limpeza e resultados parciais. Use Task.checkCancellation() para lançar imediatamente, ou verifique Task.isCancelled quando quiser lidar com cancelamento graciosamente (como retornando resultados parciais).
func processLargeDataset(_ items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
// Verifica antes de cada operação cara
try Task.checkCancellation() // Lança se cancelado
// Ou verifica sem lançar
if Task.isCancelled {
return results // Retorna resultados parciais
}
let result = await process(item)
results.append(result)
}
return results
}
Task.detached - Escapando do contexto atual
Um Task { } regular herda o contexto atual do actor - se você está no @MainActor, o task roda no @MainActor. Às vezes isso não é o que você quer, especialmente para trabalho intensivo de CPU que bloquearia a UI. Task.detached cria um task sem contexto herdado, rodando em um executor de fundo. Use com moderação - na maioria das vezes, Task regular com pontos await apropriados é suficiente e mais fácil de raciocinar.
@MainActor
class ImageProcessor {
func processImage(_ image: UIImage) {
// NÃO FAÇA: Isso ainda herda o contexto do MainActor
Task {
let filtered = applyFilters(image) // Bloqueia main!
}
// FAÇA: Task detached roda independentemente
Task.detached(priority: .userInitiated) {
let filtered = await self.applyFilters(image)
await MainActor.run {
self.displayImage(filtered)
}
}
}
}
Task.detached geralmente está errado
Na maioria das vezes, você quer um Task regular. Tasks detached não herdam prioridade, valores task-local, ou contexto de actor. Use-os apenas quando você explicitamente precisar dessa separação.
@preconcurrency - Vivendo com código legado
Silencia warnings ao importar módulos ainda não atualizados para concorrência:
// Suprime warnings desse import
@preconcurrency import OldFramework
// Ou em uma conformance de protocolo
class MyDelegate: @preconcurrency SomeOldDelegate {
// Não avisa sobre requisitos não-Sendable
}
@preconcurrency é temporário
Use como uma ponte enquanto atualiza código. O objetivo é eventualmente removê-lo e ter conformance Sendable apropriada.
Leitura Adicional
Este guia destila os melhores recursos sobre concorrência em Swift.
Blog de Matt Massicotte (Altamente Recomendado)
- A Swift Concurrency Glossary - Terminologia essencial
- An Introduction to Isolation - O conceito central
- When should you use an actor? - Guia prático
- Non-Sendable types are cool too - Por que mais simples é melhor
- Crossing the Boundary - Trabalhando com tipos não-Sendable
- Problematic Swift Concurrency Patterns - O que evitar
- Making Mistakes with Swift Concurrency - Aprendendo com erros