Este post é o primeiro de uma sequência mais longa para mostrar algumas soluções interessantes para tratar de assincronia de dados em SwiftUI1. Basicamente, o problema é quando a informação que precisamos exibir não está disponível exatamente no momento em que a tela é exibida: um caso muito comum é quando a gente faz uma requisição dos dados de uma API REST no momento em que uma tela é exibida ao usuário; enquanto a resposta não chega (o que pode demorar vários décimos de segundo), a gente precisa exibir alguma coisa para o usuário — alguma mensagem de “loading…” ou uma animação indicando que a informação “já vai chegar”.
Primeiro passo: Como armazenar um dado assíncrono?
A primeira coisa é decidir, sob o ponto de vista de programação, como representar que uma informação ainda não está disponível. À primeira vista, talvez o uso de um Optional pareça perfeito, ou seja, enquanto a informação não chega a gente guarda nil. Mais ou menos assim:
struct AsyncNameView: View {
let name: String?
var body: some View {
if let name {
Text("Nome: \(name)")
} else {
Text("Carregando…")
}
}
}
Sinceramente eu não gosto dessa abordagem, por dois motivos: Primeiro que tem algumas informações que podem ser opcionais no servidor, ou seja, a própria resposta da requisição pode retornar nil (pense nas colunas de um banco de dados que podem ser NULL) — nesses casos a gente não consegue diferenciar o estado “a informação ainda não chegou” do estado “a informação já chegou e ela é nil”. Além disso, a gente fica limitado para representar outros estados; por exemplo, indicar que teve uma falha na requisição.
Para resolver isso, eu prefiro criar o meu próprio tipo. Como a ideia é armazenar “um entre vários estados possíveis”, nada melhor do que um enum, certo? Vamos à definição:
/// Armazena o estado de um dado assíncrono
public enum AsyncData<Data> {
/// O dado ainda não está disponível
case loading
/// Houve uma falha na requisição e o dado nunca estará disponível
case failed
/// A informação está disponível
case loaded(Data)
}
Se você está programando em Swift há pouco tempo, aqui pode ter duas novidades interessantes:
- A primeira é esse tal de
<Data>logo depois do nome do tipo. O que é isso? Bom, aqui estou usando programação genérica2: em resumo, sempre que a gente usar o tipoAsyncDataa gente vai precisar dizer qual é o tipo do dado associado a ele. Ou seja, a gente declara uma variávelvar name: AsyncData<String>para ter uma string associada, ouvar age: AsyncData<Int>para ter um inteiro associado, ou mesmovar petNames: AsyncData<[String]>para ter um array de strings associado. Na verdade, qualquer tipo pode ser associado, inclusive as suas próprias classes e estruturas! - Veja que o terceiro caso deste enum,
.loaded, tem um valor associado3, indicado pelo termo “(Data)”. Basicamente significa que sempre que a gente usar o caso.loadedé necessário informar qual o valor a ser associado, que é do tipoData(que, por sua vez, é indicado por nós quando a variável foi criada).
Finalmente a view!
Bom, é um bocado de informação até agora! Vamos então voltar ao nosso código-fonte para fazer uso desse enum:
struct AsyncNameView: View {
let name: AsyncData<String>
var body: some View {
switch name {
case .loading:
Text("Carregando…")
.opacity(0.5)
case .failed:
Text("Erro na comunicação!")
.color(.red)
case .loaded(let name):
Text("Nome: \(name)")
}
}
}
Agora você já pode instanciar AsyncNameView especificando o estado da informação:
// Para gerar a view no estado “Carregando”, use:
AsyncNameView(name: .loading)
// Para gerar a view indicando que houve erro, use:
AsyncNameView(name: .failed)
// Para gerar a view com o dado carregado, use:
AsyncNameView(name: .loaded("Fulano de Tal"))
Eis as três versões na tela:



Bônus: Usando várias previews
Se, para testar este código, você começou pedindo para o Xcode criar uma view, então ele gerou uma pré-visualização automaticamente no fim do arquivo da seguinte forma:
#Preview {
AsyncNameView()
}
Se for esse o caso, você já percebeu que nessa altura do campeonato a pré-visualização de AsyncNameView parou de funcionar. Isso porque a declaração da propriedade let name: AsyncData<String> agora requer que a gente especifique algum valor para essa propriedade, e esse código automático da #Preview tenta criar a view sem nenhum parâmetro.
Até aqui não tem muito mistério: É só alterar o código do bloco da #Preview para atribuir algum valor à propriedade que a pré-visualização volta a funcionar:
#Preview {
AsyncNameView(name: .loaded("Fulano de Tal"))
}
Mas você sabia que é possível gerar várias previews ao mesmo tempo? É bem simples: Basta criar vários blocos de #Preview. Inclusive, pra manter a organização você pode dar um nome para cada bloco:
#Preview("Loaded") {
AsyncNameView(name: .loaded("Fulano de Tal"))
}
#Preview("Loading") {
AsyncNameView(name: .loading)
}
#Preview("Failed") {
AsyncNameView(name: .failed)
}
Esse recurso é bem bacana porque você pode selecionar rapidamente qual versão de pré-visualização você quer ver:

Bacana! Agora, que tal seguir para a próxima parte desta sequência de posts para ver como generalizar essa view assíncrona para qualquer tipo de dados?
- Talvez seja mais preciso usar o termo dados atrasados, já que eles chegam “atrasados” em relação ao momento em que a tela é exibida; mas vou manter o termo “dados assíncronos” porque é um efeito natural de usar programação assíncrona em geral (tanto com
async/awaitcomo com completion handlers). ↩︎ - https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ ↩︎
- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations#Associated-Values ↩︎