No artigo anterior nós vimos uma maneira de representar dados assíncronos por meio de uma enum com dois recursos interessantes: ela é genérica, de modo que pode se adaptar a qualquer tipo de dados; e ela tem um valor associado quando a informação está disponível. Vou copiar abaixo a definição dessa enum por conveniência:
/// 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) }
Nós também vimos um exemplo de uso para exibir o texto “Nome: …” quando essa informação está disponível:
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)") } } }
Só que nós temos alguns problemas nesse código aí do AsyncNameView
. Quer dizer, funciona sem bugs; mas é um código que só serve para exibir nomes — afinal, o dado assíncrono é do tipo String
(linha 2) e a renderização do dado disponível é um texto com prefixo "Nome: "
(linha 14). Mas e quando for um endereço? Ou um dado que não é do tipo String
— um inteiro ou uma imagem, por exemplo? Parece injusto ter que copiar várias vezes todo esse bloco de código acima só pra alterar as linhas 2 e 14, não é mesmo? Além disso, se no futuro decidirmos modificar essa interface (alterar a opacidade, traduzir as mensagens, descrever melhor o erro de comunicação…) a gente teria que fazer alterações em todas as versões das structs AsyncAlgumaCoisaView
.
Neste post vamos ver como conseguimos generalizar essa view para que ela possa servir para qualquer tipo de dados, em especial para gerar qualquer tipo de view (não só um Text
). Vamos lá?
Generalizando o tipo de dado da view
Uma abordagem possível é reescrever AsyncNameView
de forma genérica, de modo que ela possa aceitar qualquer tipo de dado associado a AsyncData
e também para que ela possa “renderizar” esse dado do jeito que a gente quiser.
Obviamente, para chegar aonde queremos o problema são as tais linhas 2 e 14, então vamos nos concentrar nelas. Vamos começar pela definição da propriedade, alterando seu nome para algo mais genérico — “asyncData
” — e permitindo qualquer tipo de dado associado, ou seja, deixando de associar String
:
struct AsyncDataView: View { let asyncData: AsyncData< ... opa, o que vem aqui?? ... >
Hmmm, aqui vem o primeiro problema: O que colocar no meio de <
…>
? Tem que ser alguma coisa “genérica”, certo? Bom, nós já usamos algo bem parecido na definição da própria enum AsyncData
: A gente usou o “tipo virtual” Data
como um “placeholder” para algum tipo real que o programador define depois. A gente só precisa lembrar que a gente é obrigado a “declarar” esse tipo virtual. Então precisaremos alterar as duas primeiras linhas de código:
struct AsyncDataView<Data>: View { let asyncData: AsyncData<Data> 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)") -- por enquanto deixou de funcionar } } }
Agora vamos voltar a atenção para a linha 14, que é um pouquinho mais complicada. Vamos lembrar que a ideia é permitir “renderizar” o conteúdo do jeito que a gente quiser, ou seja, não podemos nos limitar a um Text
. Por outro lado, a gente só consegue montar essa “renderização” depois que o dado estiver .loaded
— em outras palavras, é impossível informar no construtor de AsyncDataView
a aparência final da renderização, pois talvez o dado nem esteja disponível no momento da construção!
A solução para isso é: Quem criar uma instância de AsyncDataView
terá que informar, por meio de um trecho de código-fonte, o jeito certo de renderizar o conteúdo. Na verdade é um recurso que a gente usa em vários lugares, que são as tais das “closures”: no próprio trecho de código acima a gente tem na linha 5 a declaração de uma propriedade, var body
, seguida de um trecho de código-fonte (uma closure das linhas 5 a 16) que “ensina” a calcular o valor dessa propriedade.
Em resumo, a solução para uma renderização genérica é:
- Ao criar o
AsyncDataView
uma closure terá que ser informada para “ensinar” como se renderiza um dado.loaded
; - Essa closure terá que ser posteriormente “chamada” quando for a hora de renderizar.
Vamos a esses dois passos?
Armazenando a closure
Vamos começar com a parte em que essa closure é armazenada:
struct AsyncDataView<Data, LoadedView: View>: View { let asyncData: AsyncData<Data> let loadedViewGenerator: (Data) -> LoadedView
Pera, várias novidades em duas linhas de código!! Vamos analisar essas novidades:
- Na linha 1 a gente passa a ter um segundo tipo genérico: além do já conhecido
Data
, agora a gente tem tambémLoadedView
, que é o tipo resultante da renderização — pode serText
como no exemplo inicial de “Nome: …”, pode serImage
, ou até coisas mais complexas comoVStack
ouScrollView
ou uma view que nós criamos em outro lugar. A única restrição é que, ao contrário deData
(que pode ser qualquer coisa), essaLoadedView
precisa ser umaView
— é por isso que aparece a restrição “: View
” na linha 1. - Na linha 3 a gente cria uma propriedade que vai armazenar a closure. Essa propriedade não tem nada de especial: só parece esquisita porque o tipo dela é
(Data) -> LoadedView
. O que significa isso? Significa que “esta propriedade armazena uma closure que recebe um parâmetro do tipoData
e que precisa retornar umaLoadedView
”.
Parece bem complicado, especialmente quando a gente for criar uma AsyncDataView
. Na verdade o Swift consegue inferir (“adivinhar”) todos os tipos envolvidos durante a construção, e o Xcode nos ajuda bastante — a gente vê isso daqui a pouco.
Chamando a closure
Essa é a parte mais fácil de todas: Uma vez que a gente extrai o valor associado de .loaded
, é só chamar a closure da mesma maneira que a gente chama uma função qualquer:
case .loaded(let data): loadedViewGenerator(data)
Adotando AsyncDataView
no lugar de AsyncNameView
nas previews
A última coisa que falta para fecharmos é atualizar as #Preview
s que foram criadas no post anterior. O legal é que no Swift a definição da closure aparece daquela maneira clássica que já vemos em vários lugares — é um bloco de código que parece que “vem depois” do construtor (mas na verdade faz parte dele):
#Preview("Loaded") { AsyncDataView(asyncData: .loaded("Fulano de Tal")) { name in Text("Nome: \(name)") } } #Preview("Loading") { AsyncDataView(asyncData: .loading) { name in Text("Nome: \(name)") } } #Preview("Failed") { AsyncDataView(asyncData: .failed) { name in Text("Nome: \(name)") } }
Prontinho, agora as previews estão funcionando! Agora, que tal seguir para a próxima parte desta sequência de posts para ver um pouquinho de integração de Views e ViewModels?