Estupidamente Acessível
Swift Concurrency
Finalmente percebe async/await, Tasks, e porque é que o compilador não para de gritar contigo.
Enorme agradecimento a Matt Massicotte por tornar a concorrência em Swift compreensível. Compilado por Pedro Piñera, co-fundador do Tuist. Encontraste um erro? Abre uma issue ou envia um PR.
Código Assíncrono: async/await
A maior parte do que as apps fazem é esperar. Buscar dados de um servidor - esperar pela resposta. Ler um ficheiro do disco - esperar pelos bytes. Consultar uma base de dados - esperar pelos resultados.
Antes do sistema de concorrência do Swift, expressavas esta espera com callbacks, delegates, ou Combine. Funcionam, mas callbacks aninhados tornam-se difíceis de seguir, e o Combine tem uma curva de aprendizagem íngreme.
async/await dá ao Swift uma nova forma de lidar com esperas. Em vez de callbacks, escreves código que parece sequencial - pausa, espera e retoma. Por baixo, o runtime do Swift gere estas pausas de forma eficiente. Mas manter a tua app realmente responsiva enquanto esperas depende de onde o código corre, o que vamos cobrir mais tarde.
Uma função async é uma que pode precisar de pausar. Marcas com async, e quando a chamas, usas await para dizer "pausa aqui até isto acabar":
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url) // Suspende aqui
return try JSONDecoder().decode(User.self, from: data)
}
// A chamar
let user = try await fetchUser(id: 123)
// O código aqui corre depois de fetchUser completar
O teu código pausa em cada await - isto chama-se suspensão. Quando o trabalho termina, o teu código retoma exatamente onde parou. A suspensão dá ao Swift a oportunidade de fazer outro trabalho enquanto espera.
Esperar por todos
E se precisares de buscar várias coisas? Podes fazer await uma a uma:
let avatar = try await fetchImage("avatar.jpg")
let banner = try await fetchImage("banner.jpg")
let bio = try await fetchBio()
Mas isto é lento - cada uma espera que a anterior termine. Usa async let para correr em paralelo:
func loadProfile() async throws -> Profile {
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
async let bio = fetchBio()
// As três estão a buscar em paralelo!
return Profile(
avatar: try await avatar,
banner: try await banner,
bio: try await bio
)
}
Cada async let começa imediatamente. O await recolhe os resultados.
await precisa de async
Só podes usar await dentro de uma função async.
Gerir Trabalho: Tasks
Uma Task é uma unidade de trabalho async que podes gerir. Escreveste funções async, mas uma Task é o que realmente as executa. É como inicias código async a partir de código síncrono, e dá-te controlo sobre esse trabalho: esperar pelo resultado, cancelar, ou deixar correr em background.
Digamos que estás a construir um ecrã de perfil. Carrega o avatar quando a view aparece usando o modificador .task, que cancela automaticamente quando a view desaparece:
struct ProfileView: View {
@State private var avatar: Image?
var body: some View {
Group {
if let avatar {
avatar
} else {
ProgressView()
}
}
.task { avatar = await downloadAvatar() }
}
}
Se os utilizadores podem alternar entre perfis, usa .task(id:) para recarregar quando a seleção muda:
struct ProfileView: View {
var userID: String
@State private var avatar: Image?
var body: some View {
Group {
if let avatar {
avatar
} else {
ProgressView()
}
}
.task(id: userID) { avatar = await downloadAvatar(for: userID) }
}
}
Quando o utilizador carrega em "Guardar", cria uma Task manualmente:
Button("Guardar") {
Task { await saveProfile() }
}
Aceder aos resultados de Task
Quando crias uma Task, recebes um handle. Usa .value para esperar e obter o resultado:
let handle = Task {
return await fetchUserData()
}
let userData = await handle.value // Suspende até a task completar
Isto é útil quando precisas do resultado mais tarde, ou quando queres guardar o handle da task e fazer await noutro sítio.
E se precisares de carregar o avatar, bio e estatísticas de uma vez? Usa um TaskGroup para buscar em paralelo:
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { avatar = try await downloadAvatar(for: userID) }
group.addTask { bio = try await fetchBio(for: userID) }
group.addTask { stats = try await fetchStats(for: userID) }
try await group.waitForAll()
}
As Tasks dentro de um grupo são child tasks, ligadas ao pai. Algumas coisas a saber:
- O cancelamento propaga-se: cancela o pai, e todos os filhos são cancelados também
- Erros: um erro lançado cancela os irmãos e relança, mas só quando consomes resultados com
next(),waitForAll(), ou iteração - Ordem de conclusão: os resultados chegam à medida que as tasks terminam, não na ordem em que as adicionaste
- Espera por todos: o grupo não retorna até que todos os filhos completem ou sejam cancelados
Isto é concorrência estruturada: trabalho organizado numa árvore que é fácil de perceber e limpar.
Onde as Coisas Correm: De Threads a Domínios de Isolamento
Até agora falámos de quando o código corre (async/await) e como organizá-lo (Tasks). Agora: onde é que corre, e como o mantemos seguro?
A maioria das apps só espera
A maior parte do código de apps é I/O-bound. Buscas dados da rede, await uma resposta, descodificas, e mostras. Se tens múltiplas operações de I/O para coordenar, recorres a tasks e task groups. O trabalho de CPU real é mínimo. A thread principal consegue lidar com isto porque await suspende sem bloquear.
Mas mais cedo ou mais tarde, terás trabalho CPU-bound: fazer parse de um ficheiro JSON gigante, processar imagens, correr cálculos complexos. Este trabalho não espera por nada externo. Só precisa de ciclos de CPU. Se o correres na thread principal, a tua UI congela. É aqui que "onde é que o código corre" realmente importa.
O Mundo Antigo: Muitas Opções, Nenhuma Segurança
Antes do sistema de concorrência do Swift, tinhas várias formas de gerir execução:
| Abordagem | O que faz | Trade-offs |
|---|---|---|
| Thread | Controlo direto de threads | Baixo nível, propenso a erros, raramente necessário |
| GCD | Dispatch queues com closures | Simples mas sem cancelamento, fácil causar explosão de threads |
| OperationQueue | Dependências de tarefas, cancelamento, KVO | Mais controlo mas verboso e pesado |
| Combine | Streams reativos | Ótimo para streams de eventos, curva de aprendizagem íngreme |
Todos funcionavam, mas a segurança estava inteiramente nas tuas mãos. O compilador não conseguia ajudar se te esquecesses de dispatch para main, ou se duas queues acedessem aos mesmos dados simultaneamente.
O Problema: Data Races
Um data race acontece quando duas threads acedem à mesma memória ao mesmo tempo, e pelo menos uma está a escrever:
var count = 0
DispatchQueue.global().async { count += 1 }
DispatchQueue.global().async { count += 1 }
// Comportamento indefinido: crash, corrupção de memória, ou valor errado
Data races são comportamento indefinido. Podem crashar, corromper memória, ou silenciosamente produzir resultados errados. A tua app funciona bem em testes, depois crasha aleatoriamente em produção. Ferramentas tradicionais como locks e semáforos ajudam, mas são manuais e propensas a erros.
Concorrência amplifica o problema
Quanto mais concorrente a tua app é, mais prováveis se tornam os data races. Uma app iOS simples pode safar-se com segurança de threads desleixada. Um servidor web a lidar com milhares de pedidos simultâneos vai crashar constantemente. É por isso que a segurança em tempo de compilação do Swift importa mais em ambientes de alta concorrência.
A Mudança: De Threads para Isolamento
O modelo de concorrência do Swift faz uma pergunta diferente. Em vez de "em que thread é que isto deve correr?", pergunta: "quem é que tem permissão para aceder a estes dados?"
Isto é isolamento. Em vez de fazer dispatch manual de trabalho para threads, declaras fronteiras à volta de dados. O compilador aplica estas fronteiras em tempo de build, não em runtime.
Por baixo do capô
Swift Concurrency é construído em cima de libdispatch (o mesmo runtime que GCD). A diferença é a camada de tempo de compilação: actors e isolamento são aplicados pelo compilador, enquanto o runtime lida com agendamento num thread pool cooperativo limitado ao número de cores do teu CPU.
Os Três Domínios de Isolamento
1. MainActor
@MainActor é um global actor que representa o domínio de isolamento da thread principal. É especial porque frameworks de UI (UIKit, AppKit, SwiftUI) requerem acesso à thread principal.
@MainActor
class ViewModel {
var items: [Item] = [] // Protegido pelo isolamento do MainActor
}
Quando marcas algo com @MainActor, não estás a dizer "dispatch isto para a thread principal." Estás a dizer "isto pertence ao domínio de isolamento do main actor." O compilador garante que qualquer coisa que aceda a isto tem de estar no MainActor ou await para cruzar a fronteira.
Na dúvida, usa @MainActor
Para a maioria das apps, marcar os teus ViewModels com @MainActor é a escolha certa. Preocupações com performance são normalmente exageradas. Começa aqui, otimiza só se medires problemas reais.
2. Actors
Um actor protege o seu próprio estado mutável. Garante que só um pedaço de código pode aceder aos seus dados de cada vez:
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount // Seguro: actor garante acesso exclusivo
}
}
// De fora, tens de fazer await para cruzar a fronteira
await account.deposit(100)
Actors não são threads. Um actor é uma fronteira de isolamento. O runtime do Swift decide que thread realmente executa código do actor. Tu não controlas isso, e não precisas.
3. Nonisolated
Código marcado com nonisolated opta por sair do isolamento do actor. Pode ser chamado de qualquer lugar sem await, mas não pode aceder ao estado protegido do actor:
actor BankAccount {
var balance: Double = 0
nonisolated func bankName() -> String {
"Banco Acme" // Sem estado do actor acedido, seguro chamar de qualquer lugar
}
}
let name = account.bankName() // Não precisa de await
Approachable Concurrency: Menos Fricção
Approachable Concurrency simplifica o modelo mental com duas definições do Xcode:
SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor: Tudo corre no MainActor a menos que digas o contrárioSWIFT_APPROACHABLE_CONCURRENCY=YES: Funções asyncnonisolatedficam no actor do chamador em vez de saltar para uma thread de background
Novos projetos do Xcode 26 têm ambos ativados por defeito. Quando precisas de trabalho intensivo de CPU fora da thread principal, usa @concurrent.
// Corre no MainActor (o defeito)
func updateUI() async { }
// Corre em thread de background (opt-in)
@concurrent func processLargeFile() async { }
O Edifício de Escritórios
Pensa na tua app como um edifício de escritórios. Cada domínio de isolamento é um escritório privado com uma fechadura na porta. Só uma pessoa pode estar lá dentro de cada vez, a trabalhar com os documentos desse escritório.
MainActoré a receção - onde todas as interações com clientes acontecem. Só há uma, e lida com tudo o que o utilizador vê.- tipos
actorsão escritórios de departamento - Contabilidade, Jurídico, RH. Cada um protege os seus próprios documentos sensíveis. - Código
nonisolatedé o corredor - espaço partilhado por onde qualquer um pode andar, mas não há documentos privados lá.
Não podes simplesmente invadir o escritório de alguém. Bates à porta (await) e esperas que te deixem entrar.
O Que Pode Cruzar Domínios de Isolamento: Sendable
Os domínios de isolamento protegem dados, mas eventualmente precisas de passar dados entre eles. Quando o fazes, o Swift verifica se é seguro.
Pensa nisto: se passares uma referência para uma classe mutável de um actor para outro, ambos os actors podem modificá-la simultaneamente. Isso é exatamente o data race que estamos a tentar prevenir. Então o Swift precisa de saber: estes dados podem ser partilhados com segurança?
A resposta é o protocolo Sendable. É um marcador que diz ao compilador "este tipo é seguro para passar entre fronteiras de isolamento":
- Tipos Sendable podem cruzar com segurança (tipos de valor, dados imutáveis, actors)
- Tipos Non-Sendable não podem (classes com estado mutável)
// Sendable - é um tipo de valor, cada lugar recebe uma cópia
struct User: Sendable {
let id: Int
let name: String
}
// Non-Sendable - é uma classe com estado mutável
class Counter {
var count = 0 // Dois lugares a modificar isto = desastre
}
Tornar Tipos Sendable
O Swift infere automaticamente Sendable para muitos tipos:
- Structs e enums com apenas propriedades
Sendablesão implicitamenteSendable - Actors são sempre
Sendableporque protegem o seu próprio estado - tipos
@MainActorsãoSendableporque o MainActor serializa o acesso
Para classes, é mais difícil. Uma classe pode conformar a Sendable apenas se for final e todas as suas propriedades guardadas forem imutáveis:
final class APIConfig: Sendable {
let baseURL: URL // Imutável
let timeout: Double // Imutável
}
Se tiveres uma classe que é thread-safe por outros meios (locks, atomics), podes usar @unchecked Sendable para dizer ao compilador "confia em mim":
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}
@unchecked Sendable é uma promessa
O compilador não vai verificar thread safety. Se estiveres errado, vais ter data races. Usa com moderação.
Approachable Concurrency: Menos Fricção
Com Approachable Concurrency, erros de Sendable tornam-se muito mais raros:
- Se o código não cruza fronteiras de isolamento, não precisas de Sendable
- Funções async ficam no actor do chamador em vez de saltar para uma thread de background
- O compilador é mais inteligente a detetar quando valores são usados de forma segura
Ativa definindo SWIFT_DEFAULT_ACTOR_ISOLATION para MainActor e SWIFT_APPROACHABLE_CONCURRENCY para YES. Novos projetos do Xcode 26 têm ambos ativados por defeito. Quando precisas de paralelismo, marca funções com @concurrent e aí pensa em Sendable.
Fotocópias vs. Documentos Originais
De volta ao edifício de escritórios. Quando precisas de partilhar informação entre departamentos:
- Fotocópias são seguras - Se o Jurídico faz uma cópia de um documento e envia para a Contabilidade, ambos têm a sua própria cópia. Podem rabiscar nelas, modificá-las, o que quiserem. Sem conflito.
- Contratos originais assinados têm de ficar no lugar - Se dois departamentos pudessem ambos modificar o original, é o caos. Quem tem a versão real?
Tipos Sendable são como fotocópias: seguros de partilhar porque cada lugar recebe a sua própria cópia independente (tipos de valor) ou porque são imutáveis (ninguém os pode modificar). Tipos não-Sendable são como contratos originais: passá-los cria o potencial para modificações conflituantes.
Como o Isolamento é Herdado
Viste que os domínios de isolamento protegem dados, e Sendable controla o que cruza entre eles. Mas como é que o código acaba num domínio de isolamento em primeiro lugar?
Quando chamas uma função ou crias um closure, o isolamento flui através do teu código. Com Approachable Concurrency, a tua app começa no MainActor, e esse isolamento propaga-se para o código que chamas, a menos que algo o mude explicitamente. Compreender este fluxo ajuda-te a prever onde o código corre e porque é que o compilador às vezes se queixa.
Chamadas de Funções
Quando chamas uma função, o seu isolamento determina onde corre:
@MainActor func updateUI() { } // Corre sempre no MainActor
func helper() { } // Herda o isolamento do chamador
@concurrent func crunch() async { } // Corre explicitamente fora do actor
Com Approachable Concurrency, a maior parte do teu código herda isolamento do MainActor. A função corre onde o chamador corre, a menos que opte explicitamente por sair.
Closures
Closures herdam isolamento do contexto onde são definidos:
@MainActor
class ViewModel {
func setup() {
let closure = {
// Herda MainActor do ViewModel
self.updateUI() // Seguro, mesmo isolamento
}
closure()
}
}
É por isto que closures de ação de Button do SwiftUI podem atualizar @State com segurança: herdam isolamento do MainActor da view.
Tasks
Uma Task { } herda isolamento do actor de onde é criada:
@MainActor
class ViewModel {
func doWork() {
Task {
// Herda isolamento do MainActor
self.updateUI() // Seguro, não precisa de await
}
}
}
Normalmente é isto que queres. A task corre no mesmo actor que o código que a criou.
Quebrar a Herança: Task.detached
Às vezes queres uma task que não herda nenhum contexto:
@MainActor
class ViewModel {
func doHeavyWork() {
Task.detached {
// Sem isolamento de actor, corre no pool cooperativo
let result = await self.expensiveCalculation()
await MainActor.run {
self.data = result // Saltar de volta explicitamente
}
}
}
}
Task e Task.detached são antipadrões
As tasks que agendas com Task { ... } não são geridas. Não há forma de as cancelar ou saber quando terminam, se alguma vez terminarem. Não há forma de aceder ao seu valor de retorno ou saber se encontraram um erro. Na maioria dos casos, é melhor usar tasks geridas por .task ou TaskGroup, como explicado na secção "Erros Comuns".
Task.detached deve ser o teu último recurso. Tasks detached não herdam prioridade, valores task-local, ou contexto de actor. Se precisares de trabalho intensivo de CPU fora do main actor, marca a função com @concurrent em vez disso.
Preservar Isolamento em Utilitários Async
Por vezes escreves uma função async genérica que aceita um closure - um wrapper, um helper de retry, um scope de transação. O chamador passa um closure, a tua função executa-o. Simples, certo?
func measure<T>(
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
Mas quando chamas isto de um contexto @MainActor, o Swift queixa-se:
O que está a acontecer? O teu closure captura estado do MainActor, mas measure é nonisolated. O Swift vê um closure não Sendable a cruzar uma fronteira de isolamento - exatamente o que foi concebido para prevenir.
A solução mais simples é nonisolated(nonsending). Isto diz ao Swift que a função deve ficar no executor que a chamou:
nonisolated(nonsending)
func measure<T>(
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
Agora a função inteira corre no executor do chamador. Chama do MainActor, fica no MainActor. Chama de um actor personalizado, fica lá. O closure nunca cruza uma fronteira de isolamento, então não precisa de verificação Sendable.
Quando usar cada abordagem
nonisolated(nonsending) - A escolha simples. Só adiciona o atributo. Usa quando só precisas de ficar no executor do chamador.
isolation: isolated (any Actor)? = #isolation - A escolha explícita. Adiciona um parâmetro que dá acesso à instância do actor. Usa quando precisares de passar o contexto de isolamento para outras funções ou inspecionar em que actor estás.
Se precisares de acesso explícito ao actor, usa um parâmetro #isolation:
func measure<T>(
isolation: isolated (any Actor)? = #isolation,
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
Ambas as abordagens são essenciais para construir utilitários async que parecem naturais de usar. Sem elas, os chamadores precisariam tornar os seus closures @Sendable ou saltar obstáculos para satisfazer o compilador.
Andar Pelo Edifício
Quando estás no escritório da receção (MainActor), e chamas alguém para te ajudar, essa pessoa vem ao teu escritório. Herda a tua localização. Se criares uma task ("vai fazer isto por mim"), esse assistente também começa no teu escritório.
A única forma de alguém acabar num escritório diferente é se for explicitamente para lá: "Preciso de trabalhar na Contabilidade para isto" (actor), ou "Vou tratar disto no escritório de trás" (@concurrent).
Juntando Tudo
Vamos recuar e ver como todas as peças encaixam.
Swift Concurrency pode parecer muitos conceitos: async/await, Task, actors, MainActor, Sendable, domínios de isolamento. Mas há realmente só uma ideia no centro de tudo: o isolamento é herdado por defeito.
Com Approachable Concurrency ativado, a tua app começa no MainActor. Esse é o teu ponto de partida. A partir daí:
- Cada função que chamas herda esse isolamento
- Cada closure que crias captura esse isolamento
- Cada
Task { }que crias herda esse isolamento
Não tens de anotar nada. Não tens de pensar em threads. O teu código corre no MainActor, e o isolamento simplesmente propaga-se automaticamente pelo teu programa.
Quando precisas de sair dessa herança, fazes explicitamente:
@concurrentdiz "corre isto numa thread de background"actordiz "este tipo tem o seu próprio domínio de isolamento"Task.detached { }diz "começa do zero, não herda nada"
E quando passas dados entre domínios de isolamento, o Swift verifica se é seguro. É para isso que Sendable serve: marcar tipos que podem cruzar fronteiras com segurança.
É isso. É o modelo todo:
- O isolamento propaga-se do
MainActoratravés do teu código - Optas por sair explicitamente quando precisas de trabalho de background ou estado separado
- Sendable guarda as fronteiras quando dados cruzam entre domínios
Quando o compilador se queixa, está a dizer-te que uma destas regras foi violada. Rastreia a herança: de onde veio o isolamento? Onde é que o código está a tentar correr? Que dados estão a cruzar uma fronteira? A resposta normalmente é óbvia quando fazes a pergunta certa.
Para Onde Ir a Partir Daqui
A boa notícia: não precisas de dominar tudo de uma vez.
A maioria das apps só precisa do básico. Marca os teus ViewModels com @MainActor, usa async/await para chamadas de rede, e cria Task { } quando precisares de iniciar trabalho async a partir de um toque de botão. É só isso. Isto cobre 80% das apps do mundo real. O compilador dir-te-á se precisares de mais.
Quando precisares de trabalho paralelo, recorre a async let para buscar múltiplas coisas de uma vez, ou TaskGroup quando o número de tasks é dinâmico. Aprende a lidar com cancelamento de forma graciosa. Isto cobre apps com carregamento de dados complexo ou funcionalidades em tempo real.
Padrões avançados vêm depois, se alguma vez. Actors personalizados para estado mutável partilhado, @concurrent para processamento intensivo de CPU, compreensão profunda de Sendable. Isto é código de framework, Swift do lado do servidor, apps desktop complexas. A maioria dos developers nunca precisa deste nível.
Começa simples
Não otimizes para problemas que não tens. Começa com o básico, lança a tua app, e adiciona complexidade apenas quando tiveres problemas reais. O compilador vai guiar-te.
Atenção: Erros Comuns
Pensar que async = background
// Isto AINDA bloqueia a thread principal!
@MainActor
func slowFunction() async {
let result = expensiveCalculation() // Trabalho síncrono = bloqueante
data = result
}
async significa "pode pausar." O trabalho real ainda corre onde corre. Usa @concurrent (Swift 6.2) ou Task.detached para trabalho pesado de CPU.
Criar demasiados actors
// Sobre-engenharia
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }
// Melhor - a maioria das coisas pode viver no MainActor
@MainActor
class AppState { }
Só precisas de um actor personalizado quando tens estado mutável partilhado que não pode viver no MainActor. A regra do Matt Massicotte: introduz um actor apenas quando (1) tens estado não-Sendable, (2) operações nesse estado têm de ser atómicas, e (3) essas operações não podem correr num actor existente. Se não conseguires justificar, usa @MainActor em vez disso.
Tornar tudo Sendable
Nem tudo precisa de cruzar fronteiras. Se estás a adicionar @unchecked Sendable em todo o lado, recua e pergunta se os dados realmente precisam de se mover entre domínios de isolamento.
Usar MainActor.run quando não é preciso
// Desnecessário
Task {
let data = await fetchData()
await MainActor.run {
self.data = data
}
}
// Melhor - só marca a função com @MainActor
@MainActor
func loadData() async {
self.data = await fetchData()
}
MainActor.run raramente é a solução certa. Se precisas de isolamento do MainActor, anota a função com @MainActor em vez disso. É mais claro e o compilador pode ajudar-te mais. Vê a opinião do Matt sobre isto.
Bloquear o thread pool cooperativo
// NUNCA faças isto - arrisca deadlock
func badIdea() async {
let semaphore = DispatchSemaphore(value: 0)
Task {
await doWork()
semaphore.signal()
}
semaphore.wait() // Bloqueia uma thread cooperativa!
}
O thread pool cooperativo do Swift tem threads limitadas. Bloquear uma com DispatchSemaphore, DispatchGroup.wait(), ou chamadas similares pode causar deadlocks. Se precisares de fazer bridge entre código sync e async, usa async let ou reestrutura para ficar completamente async.
Criar tasks não geridas
Tasks que crias manualmente com Task { ... } ou Task.detached { ... } não são geridas. Depois de criares tasks não geridas, não as podes controlar. Não as podes cancelar se a task de onde as iniciaste for cancelada. Não podes saber se terminaram o trabalho, se lançaram um erro, ou recolher o valor de retorno. Iniciar tal task é como atirar uma garrafa ao mar esperando que entregue a mensagem ao destino, sem nunca mais ver essa garrafa.
O Edifício de Escritórios
Uma Task é como atribuir trabalho a um funcionário. O funcionário trata do pedido (incluindo esperar por outros escritórios) enquanto continuas com o teu trabalho imediato.
Depois de enviares trabalho para o funcionário, não tens meios de comunicar com ela. Não podes dizer-lhe para parar o trabalho ou saber se terminou e qual foi o resultado desse trabalho.
O que realmente queres é dar ao funcionário um walkie-talkie para comunicares com ela enquanto trata do pedido. Com o walkie-talkie, podes dizer-lhe para parar, ou ela pode dizer-te quando encontrar um erro, ou pode reportar o resultado do pedido que lhe deste.
Em vez de criares tasks não geridas, usa concorrência Swift para manteres o controlo das subtasks que crias. Usa TaskGroup para gerir (um grupo de) subtask(s). Swift fornece algumas funções withTaskGroup() { group in ... } para ajudar a criar grupos de tasks.
func doWork() async {
// isto retornará quando todas as subtasks retornarem, lançarem um erro, ou forem canceladas
let result = try await withThrowingTaskGroup() { group in
group.addTask {
try await self.performAsyncOperation1()
}
group.addTask {
try await self.performAsyncOperation2()
}
// espera e recolhe os resultados das tasks aqui
}
}
func performAsyncOperation1() async throws -> Int {
return 1
}
func performAsyncOperation2() async throws -> Int {
return 2
}
Para recolheres os resultados das tasks filhas do grupo, podes usar um loop for-await-in:
var sum = 0
for await result in group {
sum += result
}
// sum == 3
Podes aprender mais sobre TaskGroup na documentação do Swift.
Nota sobre Tasks e SwiftUI.
Ao escreveres uma UI, frequentemente queres iniciar tasks assíncronas de um contexto síncrono. Por exemplo, queres carregar uma imagem de forma assíncrona como resposta a um toque num elemento de UI. Iniciar tasks assíncronas de um contexto síncrono não é possível em Swift. Por isso vês soluções envolvendo Task { ... }, que introduz tasks não geridas.
Não podes usar TaskGroup de um modifier síncrono do SwiftUI porque withTaskGroup() também é uma função async e as suas funções relacionadas também são.
Como alternativa, SwiftUI oferece um modifier assíncrono que podes usar para iniciar operações assíncronas. O modifier .task { }, que já mencionámos, aceita uma função () async -> Void, ideal para chamar outras funções async. Está disponível em toda View. É acionado antes da view aparecer e as tasks que cria são geridas e vinculadas ao ciclo de vida da view, significando que as tasks são canceladas quando a view desaparece.
Voltando ao exemplo de tocar-para-carregar-uma-imagem: em vez de criares uma task não gerida para chamar uma função assíncrona loadImage() de uma função síncrona .onTap() { ... }, podes alternar uma flag no gesto de tap e usar o modifier task(id:) para carregar imagens de forma assíncrona quando o valor do id (a flag) mudar.
Aqui está um exemplo:
struct ContentView: View {
@State private var shouldLoadImage = false
var body: some View {
Button("Clica Aqui!") {
// alterna a flag
shouldLoadImage = !shouldLoadImage
}
// a View gere a subtask
// inicia antes da view ser exibida
// e para quando a view é ocultada
.task(id: shouldLoadImage) {
// quando o valor da flag muda, SwiftUI reinicia a task
guard shouldLoadImage else { return }
await loadImage()
}
}
}
Folha de Consulta: Referência Rápida
| Palavra-chave | O que faz |
|---|---|
async |
Função pode pausar |
await |
Pausa aqui até terminar |
Task { } |
Inicia trabalho async, herda contexto |
Task.detached { } |
Inicia trabalho async, sem contexto herdado |
@MainActor |
Corre na thread principal |
actor |
Tipo com estado mutável isolado |
nonisolated |
Opta por sair do isolamento do actor |
nonisolated(nonsending) |
Fica no executor do chamador |
Sendable |
Seguro para passar entre domínios de isolamento |
@concurrent |
Corre sempre em background (Swift 6.2+) |
#isolation |
Captura o isolamento do chamador como parâmetro |
async let |
Inicia trabalho paralelo |
TaskGroup |
Trabalho paralelo dinâmico |
Leitura Adicional
Blog do Matt Massicotte (Altamente Recomendado)
- A Swift Concurrency Glossary - Terminologia essencial
- An Introduction to Isolation - O conceito central
- When should you use an actor? - Orientação prática
- Non-Sendable types are cool too - Porque simples é melhor
Recursos Oficiais da Apple
Ferramentas
- Tuist - Desenvolve mais rápido com equipas e projetos maiores
Skill para Agentes IA
Queres que o teu assistente de código IA compreenda Swift Concurrency? Fornecemos um ficheiro SKILL.md que empacota estes modelos mentais para agentes IA como Claude Code, Codex, Amp, OpenCode e outros.
Outras skills
O que é um Skill?
Um skill é um ficheiro markdown que ensina conhecimentos especializados a agentes de código IA. Quando adicionas o skill de Swift Concurrency ao teu agente, ele aplica automaticamente estes conceitos quando te ajuda a escrever código Swift assíncrono.
Como Usar
Escolhe o teu agente e executa os comandos:
# Skill pessoal (todos os teus projetos)
mkdir -p ~/.claude/skills/swift-concurrency
curl -o ~/.claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Skill de projeto (apenas este projeto)
mkdir -p .claude/skills/swift-concurrency
curl -o .claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções globais (todos os teus projetos)
curl -o ~/.codex/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções de projeto (apenas este projeto)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Instruções de projeto (recomendado)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Regras globais (todos os teus projetos)
mkdir -p ~/.config/opencode
curl -o ~/.config/opencode/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# Regras de projeto (apenas este projeto)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
O skill inclui a analogia do Edifício de Escritórios, padrões de isolamento, guia de Sendable, erros comuns e tabelas de referência rápida. O teu agente usará este conhecimento automaticamente quando trabalhares com código Swift Concurrency.