Punti chiave
Pull-to-Refresh è un pattern dell'interfaccia utente in cui l'utente trascina (pull down) un elenco o un'area scorrevole verso il basso per aggiornare il contenuto. Visivamente, il gesto è accompagnato da un indicatore di caricamento (spinner) che appare nella parte superiore dello schermo e scompare dopo aver ricevuto i dati. Il pattern è stato reso popolare dall'app Tweetie per iPhone (2008) e successivamente standardizzato da Apple (iOS 6 — UIRefreshControl) e Google (Android Support Library — SwipeRefreshLayout).
Da un punto di vista tecnico, il Pull-to-Refresh è una combinazione di panning (tracciamento dello spostamento del dito) e un trigger al raggiungimento di una soglia. L'utente trascina l'elenco verso il basso, superando una resistenza (overscroll resistivo), e dopo aver superato la soglia (~80px su iOS, ~64dp su Android), iniziano l'animazione dell'indicatore e il caricamento asincrono. Se l'utente rilascia il dito prima della soglia, l'elenco torna nella posizione originale senza aggiornarsi.
Secondo le Material Design Guidelines, il Pull-to-Refresh non dovrebbe essere utilizzato per la navigazione o il cambio di schede — il suo unico scopo è l'aggiornamento dei dati. In IT Sectr, utilizziamo il Pull-to-Refresh nei feed di notizie, negli elenchi di ordini e nelle chat dove la freschezza dei dati è critica per l'esperienza utente.
UIRefreshControl è il controllo standard iOS per il Pull-to-Refresh, disponibile da iOS 6. UIRefreshControl viene aggiunto a UITableViewController tramite la proprietà refreshControl (iOS 10+) o come subview della tabella nelle versioni precedenti. Include uno spinner integrato con colore personalizzabile (tintColor), attributo title e una stringa attribuita con un'etichetta (ad esempio, "Aggiornamento in corso...").
UIRefreshControl funziona tramite il meccanismo target-action: quando il gesto viene attivato, viene chiamato il metodo specificato (ad esempio, refresh(_:)). All'interno del metodo, viene eseguito il caricamento asincrono dei dati. Al termine, viene chiamato endRefreshing(), che nasconde l'indicatore con un'animazione. UIRefreshControl gestisce automaticamente la sensibilità del gesto — si attiva solo quando la tabella è in posizione superiore (contentOffset.y <= 0).
La proprietà tintColor imposta il colore dello spinner. attributedTitle permette di mostrare testo come "Aggiornato 2 minuti fa" al termine. Da iOS 10, UIRefreshControl supporta animazioni personalizzate tramite UIActivityIndicatorView o viste personalizzate persistenti. In IT Sectr, configuriamo tintColor in base al marchio e mostriamo l'ora dell'ultimo aggiornamento tramite attributedTitle — questo aumenta la fiducia degli utenti nei dati.
SwipeRefreshLayout è un ViewGroup della Android Support Library (androidx.swiperefreshlayout) che avvolge il contenuto scorrevole (RecyclerView, NestedScrollView, ListView) e aggiunge la funzionalità Pull-to-Refresh. A differenza di UIRefreshControl (che è un controllo, non un contenitore), SwipeRefreshLayout è un contenitore che intercetta gli eventi touch del figlio e attiva l'indicatore di aggiornamento quando viene superata la soglia.
SwipeRefreshLayout utilizza un indicatore di progresso circolare Material Design con personalizzazione del colore tramite setColorSchemeColors(). Il metodo setOnRefreshListener imposta il callback onRefresh(), in cui viene eseguito il caricamento asincrono. Al termine, viene chiamato setRefreshing(false) per nascondere l'indicatore. Importante: setRefreshing(true) chiama nuovamente onRefresh() — quindi per avviare l'aggiornamento a livello di codice, utilizzare un flag o un metodo post.
La proprietà setProgressBackgroundColorSchemeResource modifica lo sfondo dell'indicatore. setSize(SwipeRefreshLayout.LARGE) imposta la dimensione dello spinner. Nel layout XML, SwipeRefreshLayout avvolge RecyclerView: swipe_refresh_layout → recycler_view. Secondo Google I/O 2024, SwipeRefreshLayout è utilizzato nell'85% delle app Android con feed di contenuti. In IT Sectr, avvolgiamo tutti gli schermi con elenchi caricati in modo asincrono in SwipeRefreshLayout — questo fornisce un'esperienza utente coerente su tutte le versioni di Android.
A partire da Android 12 (Material You), Google consiglia di utilizzare il nuovo Material Pull-to-Refresh dalla libreria material-1.6.0+ (androidx.compose.material3.pulltorefresh per Compose). La nuova API utilizza un indicatore animato con supporto per animazione spring e colore adattivo basato sullo sfondo. SwipeRefreshLayout rimane compatibile per le versioni inferiori ad Android 12.
Il Pull-to-Refresh è un pattern semplice da implementare, ma contiene diversi errori tipici che degradano l'esperienza utente. Esaminiamoli e come evitarli.
In IT Sectr, abbiamo aggiunto il controllo isRefreshing in ogni progetto dopo aver scoperto richieste duplicate nei log del server di test — si è scoperto che gli utenti con dita veloci attivavano l'aggiornamento fino a 3 volte di seguito.
Aggiunge Pull-to-Refresh a UITableViewController con colore dello spinner personalizzato e titolo attribuito. Dopo il caricamento dei dati, l'indicatore viene nascosto.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: "Trascina per aggiornare"
)
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()
}
}
}
La proprietà tableView.refreshControl (iOS 10+) imposta UIRefreshControl. addTarget con l'evento .valueChanged si attiva quando il gesto viene attivato. endRefreshing() è obbligatorio — senza di esso, l'indicatore gira all'infinito. Il caricamento asincrono è simulato con DispatchQueue.main.asyncAfter — in un progetto reale, utilizzare URLSession o async/await.
Avvolge RecyclerView in SwipeRefreshLayout con colori dell'indicatore personalizzati. onRefresh avvia il caricamento e nasconde l'indicatore al termine.
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 imposta i colori dell'indicatore rotante Material Design. isRefreshing = false viene chiamato nel blocco finally per nascondere l'indicatore anche in caso di errore di caricamento. ViewModelScope.launch esegue una coroutine all'interno del ciclo di vita del fragment — quando il fragment viene distrutto, la coroutine viene automaticamente annullata, prevenendo perdite di memoria.
SwiftUI moderno fornisce il modificatore .refreshable che aggiunge automaticamente Pull-to-Refresh a List o 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()
}
}
}
Il modificatore .refreshable accetta una closure asincrona che viene eseguita al Pull-to-Refresh. SwiftUI mostra e nasconde automaticamente l'indicatore di aggiornamento, gestisce le race condition (non avvia un nuovo aggiornamento fino al completamento di quello corrente) e adatta l'animazione alla piattaforma. Per iOS 15+, questo è il modo preferito per implementare Pull-to-Refresh in SwiftUI.
Domande frequenti
Sì, SwiftUI fornisce il modificatore .refreshable per List o ScrollView, disponibile da iOS 15. All'interno della closure, viene eseguito il codice asincrono di caricamento dati. SwiftUI gestisce automaticamente l'indicatore di aggiornamento e blocca le attivazioni ripetute fino al completamento del caricamento corrente — questo è l'approccio standard raccomandato per i nuovi progetti.
Utilizzare un flag isRefreshing: impostarlo a true all'avvio del caricamento e a false al termine. Su iOS, UIRefreshControl blocca automaticamente le chiamate ripetute fino a quando non viene chiamato endRefreshing(). Su Android, verificare SwipeRefreshLayout.isRefreshing all'inizio di onRefresh(): se è true — return. Questo garantisce una richiesta per gesto.
UIRefreshControl e SwipeRefreshLayout si attivano solo quando l'elenco è in posizione superiore (contentOffset == 0). L'architettura elimina il conflitto: finché l'elenco è scorrevole anche solo di 1px, il gesto Pull-to-Refresh non si attiva. Se si verifica un conflitto, verificare nestedScrollingEnabled in Android o la presenza di GestureRecognizer personalizzati che intercettano i tocchi.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche