Pontos principais
Pull-to-Refresh é um padrão de interface do usuário onde o usuário puxa (pull down) uma lista ou área rolável para baixo para atualizar o conteúdo. Visualmente, o gesto é acompanhado por um indicador de carregamento (spinner) que aparece no topo da tela e desaparece após receber os dados. O padrão foi popularizado pelo aplicativo Tweetie para iPhone (2008) e posteriormente padronizado pela Apple (iOS 6 — UIRefreshControl) e Google (Android Support Library — SwipeRefreshLayout).
Do ponto de vista técnico, Pull-to-Refresh é uma combinação de pan (rastreamento do deslocamento do dedo) e um gatilho ao atingir um limite. O usuário puxa a lista para baixo, superando uma resistência (overscroll resistivo), e após exceder o limite (~80px no iOS, ~64dp no Android), a animação do indicador e o carregamento assíncrono são iniciados. Se o usuário soltar o dedo antes do limite, a lista retorna à sua posição original sem atualizar.
De acordo com as Material Design Guidelines, o Pull-to-Refresh não deve ser usado para navegação ou troca de abas — seu único propósito é a atualização de dados. Na IT Sectr, usamos Pull-to-Refresh em feeds de notícias, listas de pedidos e chats onde a atualidade dos dados é crítica para a experiência do usuário.
UIRefreshControl é o controle padrão do iOS para Pull-to-Refresh, disponível desde o iOS 6. O UIRefreshControl é adicionado ao UITableViewController através da propriedade refreshControl (iOS 10+) ou como uma subview da tabela em versões anteriores. Ele inclui um spinner embutido com cor personalizável (tintColor), atributo title e uma string atribuída com um rótulo (por exemplo, "Atualizando...").
O UIRefreshControl funciona através do mecanismo target-action: quando o gesto é ativado, o método especificado é chamado (por exemplo, refresh(_:)). Dentro do método, o carregamento assíncrono de dados é realizado. Após a conclusão, endRefreshing() é chamado, que oculta o indicador com animação. O UIRefreshControl gerencia automaticamente a sensibilidade do gesto — ele só é acionado quando a tabela está na posição superior (contentOffset.y <= 0).
A propriedade tintColor define a cor do spinner. O attributedTitle permite mostrar texto como "Atualizado há 2 minutos" após a conclusão. Desde o iOS 10, o UIRefreshControl suporta animações personalizadas através de UIActivityIndicatorView ou visualizações personalizadas persistentes. Na IT Sectr, configuramos o tintColor de acordo com a marca e mostramos o horário da última atualização através do attributedTitle — isso aumenta a confiança do usuário nos dados.
SwipeRefreshLayout é um ViewGroup da Android Support Library (androidx.swiperefreshlayout) que envolve conteúdo rolável (RecyclerView, NestedScrollView, ListView) e adiciona funcionalidade Pull-to-Refresh. Ao contrário do UIRefreshControl (que é um controle, não um contêiner), o SwipeRefreshLayout é um contêiner que intercepta os eventos de toque do filho e aciona o indicador de atualização quando o limite é excedido.
O SwipeRefreshLayout usa um indicador de progresso circular do Material Design com personalização de cor através de setColorSchemeColors(). O método setOnRefreshListener define o callback onRefresh(), no qual o carregamento assíncrono é realizado. Após a conclusão, setRefreshing(false) é chamado para ocultar o indicador. Importante: setRefreshing(true) chama onRefresh() novamente — portanto, para iniciar a atualização programaticamente, use uma flag ou método post.
A propriedade setProgressBackgroundColorSchemeResource altera o fundo do indicador. setSize(SwipeRefreshLayout.LARGE) define o tamanho do spinner. No layout XML, o SwipeRefreshLayout envolve o RecyclerView: swipe_refresh_layout → recycler_view. De acordo com o Google I/O 2024, o SwipeRefreshLayout é usado em 85% dos aplicativos Android com feeds de conteúdo. Na IT Sectr, envolvemos todas as telas com listas carregadas assincronamente em SwipeRefreshLayout — isso proporciona uma experiência de usuário consistente em todas as versões do Android.
A partir do Android 12 (Material You), o Google recomenda usar o novo Material Pull-to-Refresh da biblioteca material-1.6.0+ (androidx.compose.material3.pulltorefresh para Compose). A nova API usa um indicador animado com suporte a animação spring e cor adaptativa baseada no papel de parede. O SwipeRefreshLayout permanece compatível para versões abaixo do Android 12.
Pull-to-Refresh é um padrão simples de implementar, mas contém vários erros típicos que degradam a experiência do usuário. Vamos examiná-los e como evitá-los.
Na IT Sectr, adicionamos a verificação isRefreshing em todos os projetos depois de descobrir requisições duplicadas nos logs do servidor de teste — descobriu-se que usuários com dedos rápidos estavam acionando a atualização até 3 vezes seguidas.
Adiciona Pull-to-Refresh ao UITableViewController com cor de spinner personalizada e título atribuído. Após o carregamento dos dados, o indicador é ocultado.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: "Puxe para atualizar"
)
refreshControl?.addTarget(
self,
action: #selector(refreshData),
for: .valueChanged
)
}
@objc private func refreshData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.items = FeedService().fetchLatest()
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
}
}
}
A propriedade tableView.refreshControl (iOS 10+) define o UIRefreshControl. addTarget com o evento .valueChanged é acionado quando o gesto é ativado. endRefreshing() é obrigatório — sem ele, o indicador gira indefinidamente. O carregamento assíncrono é simulado com DispatchQueue.main.asyncAfter — em um projeto real, use URLSession ou async/await.
Envolve RecyclerView em SwipeRefreshLayout com cores de indicador personalizadas. onRefresh inicia o carregamento e oculta o indicador após a conclusão.
class FeedFragment : Fragment() {
private var _binding: FragmentFeedBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFeedBinding.inflate(inflater, container, false)
binding.swipeRefreshLayout.setColorSchemeColors(
resources.getColor(R.color.brand_blue, null),
resources.getColor(R.color.brand_green, null)
)
binding.swipeRefreshLayout.setOnRefreshListener {
loadData()
}
return binding.root
}
private fun loadData() {
viewModelScope.launch {
try {
val result = repository.getLatestFeed()
adapter.submitList(result)
} finally {
binding.swipeRefreshLayout.isRefreshing = false
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
setColorSchemeColors define as cores do indicador giratório do Material Design. isRefreshing = false é chamado no bloco finally para ocultar o indicador mesmo em caso de erro de carregamento. ViewModelScope.launch executa uma corrotina dentro do ciclo de vida do fragmento — quando o fragmento é destruído, a corrotina é cancelada automaticamente, evitando vazamentos de memória.
O SwiftUI moderno fornece o modificador .refreshable que adiciona automaticamente Pull-to-Refresh a List ou ScrollView.
import SwiftUI
struct FeedView: View {
@State private var items: [String] = []
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
.refreshable {
items = await FeedService().fetchLatestAsync()
}
}
}
O modificador .refreshable aceita um async-closure que executa no Pull-to-Refresh. O SwiftUI automaticamente mostra e oculta o indicador de atualização, gerencia condições de corrida (não inicia uma nova atualização até que a atual termine) e adapta a animação à plataforma. Para iOS 15+, esta é a maneira preferida de implementar Pull-to-Refresh no SwiftUI.
Perguntas frequentes
Sim, o SwiftUI fornece o modificador .refreshable para List ou ScrollView, disponível desde o iOS 15. Dentro do closure, o código assíncrono de carregamento de dados é executado. O SwiftUI gerencia automaticamente o indicador de atualização e bloqueia ativações repetidas até que o carregamento atual termine — esta é a abordagem recomendada padrão para novos projetos.
Use uma flag isRefreshing: defina como true no início do carregamento e false após a conclusão. No iOS, o UIRefreshControl bloqueia automaticamente chamadas repetidas até que endRefreshing() seja chamado. No Android, verifique SwipeRefreshLayout.isRefreshing no início de onRefresh(): se for true — return. Isso garante uma requisição por gesto.
UIRefreshControl e SwipeRefreshLayout só são acionados quando a lista está na posição superior (contentOffset == 0). A arquitetura elimina o conflito: enquanto a lista estiver rolada mesmo que 1px, o gesto Pull-to-Refresh não é ativado. Se ocorrer um conflito, verifique nestedScrollingEnabled no Android ou a presença de GestureRecognizers personalizados interceptando toques.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também