Implementando paginação (infinite scroll) em Swift
Imagine se as postagens de todas as pessoas que você segue no instagram fossem carregadas de uma única vez ao abrir o aplicativo. Certamente, nem sua internet e nem a memória do seu celular aguentariam.
É por esses (e outros) motivos que a paginação é uma importante ferramenta na hora de criar e de consumir os dados de um API.
A paginação se trata da fragmentação do envio dos dados consumidos de um servidor, diminuindo assim o tempo de resposta da requisição, bem como da quantidade de dados consumidos.
Neste post, veremos algumas formas de implementar a paginação utilizando a rolagem de uma tableView em Swift.
É importante entendermos que a maneira que iremos implementar a paginação depende diretamente da forma como o backend implementou a API. Há algumas maneiras de arquitetar a fragmentação dos dados, aqui utilizaremos o Page-PageSize ou Page-Limit.
Page-PageSize (Page-Limit)
Este tipo de paginação é baseada no número de páginas (page) e na quantidade de dados que serão enviados por página (pageSize ou limit).
ex: http://minha.api/dados?page=2&page_size=50 ou http://minha.api/dados?page=2&limit=50
Neste exemplo, cada nova requisição será feita através da próxima página e a quantidade de dados carregados por página será 50.
Agora chega de conversa, vamos para o código!
Implementando a paginação em Swift
Vamos criar um app que carregará uma lista simples com 50 ítens por página, sendo as páginas carregadas quando o(a) usuário(a) chegar nas últimas células da tableView.
Para esta implementação foi utilizada a API pública:
https://api.artic.edu/api/v1/artworks
Ao abrir o link no navegador conseguimos ver o objeto que é retornado na requisição:
Como não enviamos nenhum parâmetro de paginação, a API retornou, por padrão, 12 itens a partir do índice 0 consequentemente começando da página 1. Os atributos retornados no array "pagination" serão utilizados para controlar as próximas requisições. É importante lembrar que os nomes e a forma do array de paginação irão depender de cada API.
Modelo
Para encapsular os dados recebidos da API, criamos o PaginationModel
que conforma com o protocolo Decodable
.
Além disso, foi implementado o enum CodingKeys
, conformando com o protocolo CodingKey
para que nossas variáveis pudessem ser utilizadas com a tipografia camelCase. Para implementar a paginação, vamos utilizar apenas uma das propriedades recebidas do retorno da requisição, neste caso escolhemos o "title".
Implementação da requisição
Para a paginação Page-Limit, os parâmetros page e limit deverão ser passados na função request e desta maneira serem adicionados ao path da urlString
da requisição.
Na classe PaginationViewModel
foi criado o método getDataPage(limit: Int)
, que é responsável por chamar a requisição no PaginationService
e tratar os retornos de sucesso e falha.
Vamos detalhar cada parte deste método:
Esta função será chamada toda vez que for necessário carregar novos dados vindos da lista da API. Para isso, criamos a propriedade page
que será utilizada no service.request()
para chamar a próxima página e será incrementada sempre ao fim do sucesso da requisição, garantindo que a próxima página seja chamada no path da próxima requisição.
Para garantir que não ocorram múltiplas requisições de um vez só, utilizamos a flag hasRequestInProgress
que será verificada na entrada da função getDataPage
. A flag será atribuída como true
antes do início da requisição e como false
ao final.
Os dados recebidos da API são armazenados no array items
a cada requisição utilizando o método .append(contentsOf: Item)
. A comunicação entre a camada de negócio e a camada de view ( PaginationViewController
) foi realizada por meio do protocolo PaginationViewModelDelegate
:
Temos três outros métodos na nossa viewModel:
- O
getNumberOfPages()
, que retorna o número de páginas enviados pela API. Utilizaremos esta informação para travar as requisições quando todas as paginas forem carregadas; - O
getMoreData(limit: Int)
é o método que será chamado para receber os dados da próxima página. Neste método verificamos se existem páginas a serem carregadas. Caso sim, ogetDataPage()
é chamado para buscar os dados da página seguinte; - E por fim temos o método
refreshData()
que será usado para reiniciar opage
e limpar o arrayitems
quando implementarmos oUIRefreshControl()
da tableView.
Ok, já criamos a máquina por trás da paginação. Agora vamos entender como faremos a requisição da próxima página quando o(a) usuário(a) chegar no final da lista e como barraremos esta chamada quando todos os dados já tiverem sido carregados.
Implementando a paginação na ViewController
A nossa estratégia é fazer com que uma nova requisição seja feita quando as últimas informações de cada pagina forem aparecer na tela. Para tal, utilizaremos o método willDisplay
existente no protocolo UITableViewDelegate
da tableView. Este método é executado quando uma célula específica for apresentada na tela do(a) usuário(a).
Precisamos saber quantos itens já foram carregados ( viewModel.items.count
) para que possamos implementar nosso método da requisição quando os últimos itens forem apresentados na tela. No código acima, quando faltarem 3 células a serem apresentadas, os métodos setupLoading()
e getMoreData()
serão chamados. O getMoreData()
é nosso velho conhecido, já o setupLoading()
é responsável por configurar o UIAticivatiorView()
no footer da tableView.
Ao final da requisição será necessário retirar este loading, certo? Lembra do nosso protocolo PaginationViewModelDelegate
? Faremos nossa controller conformar com esse protocolo e assim podemos implementar os métodos didSuccess()
, didFailure()
e dismissLoading()
.
O didSuccess(items: [Item])
é chamado no sucesso da requisição lá na PaginationViewModel
e recebe o array de itens. É neste método que chamamos também o tableView.reloadData()
para recarregar a tabela com os dados recebidos. Como a atualização ocorre na View, ela deve ser feita na main thread, por isso está dentro do DispatchQueue.
O didFailure(error: String)
está apenas imprimindo o error, caso a requisição falhe.
E, por fim, o dismissLoading()
que é chamado no retorno da requisição da viewModel. Aqui são implementados os métodos para remover os loadings do footer e do refresh.
Mas, como implementamos o Refresh?
Basta configurar a ação do refresh pelo método .addTarget()
e adicionar o refreshControl na hierarquia da tableView ( .addSubview
).
O método refresh()
remove os itens armazenados na viewController, chama o refreshData()
na viewModel e refaz a requisição para carregar os itens da primeira página.
E assim terminamos a implementação da paginação. Caso queira ver o código completo basta acessar o repositório no GitHub: InfinitScrollSwift.
Se foi útil para você, clique no clap 👏 e compartilhe com a comunidade 🥰.