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 tipoAsyncData
a 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
/await
como 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 ↩︎