Assincronia de dados em SwiftUI Views

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
struct AsyncNameView: View {
let name: String?
var body: some View {
if let name {
Text("Nome: \(name)")
} else {
Text("Carregando…")
}
}
}
struct AsyncNameView: View { let name: String? var body: some View { if let name { Text("Nome: \(name)") } else { Text("Carregando…") } } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/// 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)
}
/// 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) }
/// 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:

  1. 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 tipo AsyncData a gente vai precisar dizer qual é o tipo do dado associado a ele. Ou seja, a gente declara uma variável var name: AsyncData<String> para ter uma string associada, ou var age: AsyncData<Int> para ter um inteiro associado, ou mesmo var 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!
  2. 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 tipo Data (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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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)")
}
}
}
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)") } } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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"))
// 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"))
// 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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#Preview {
AsyncNameView()
}
#Preview { AsyncNameView() }
#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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#Preview {
AsyncNameView(name: .loaded("Fulano de Tal"))
}
#Preview { AsyncNameView(name: .loaded("Fulano de Tal")) }
#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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#Preview("Loaded") {
AsyncNameView(name: .loaded("Fulano de Tal"))
}
#Preview("Loading") {
AsyncNameView(name: .loading)
}
#Preview("Failed") {
AsyncNameView(name: .failed)
}
#Preview("Loaded") { AsyncNameView(name: .loaded("Fulano de Tal")) } #Preview("Loading") { AsyncNameView(name: .loading) } #Preview("Failed") { AsyncNameView(name: .failed) }
#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?

  1. 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). ↩︎
  2. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ ↩︎
  3. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations#Associated-Values ↩︎

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *


Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.