Pull-to-Refresh: basi, RefreshControl e UIRefreshControl

Autore: IT Sectr Pubblicato: 2026-02-27 Tempo di lettura: 8 min
Il Pull-to-Refresh è un pattern di interfaccia mobile in cui l'utente trascina l'elenco verso il basso con il dito, attivando il caricamento di nuovi dati. Il gesto è accompagnato da un indicatore visivo — uno spinner rotante o un'icona animata — che scompare al termine del caricamento. Secondo l'analisi UX di Apple HIG, il Pull-to-Refresh è diventato il meccanismo standard di aggiornamento dei contenuti nei feed di notizie, nei social network e nei client di posta elettronica dalla sua introduzione in Tweetie (2008) e dalla successiva standardizzazione da parte di Apple e Google.

Punti chiave

  • Il Pull-to-Refresh è un gesto di trascinamento dell'elenco verso il basso per aggiornare i dati, accompagnato da un indicatore visivo di caricamento.
  • Su iOS si usa UIRefreshControl (iOS 6+), aggiunto a UITableViewController o UIScrollView tramite la proprietà refreshControl.
  • Su Android si usa SwipeRefreshLayout (dalla Support Library) — un wrapper ViewGroup per RecyclerView o NestedScrollView.
  • Entrambe le API supportano la personalizzazione di colori, indicatori e callback tramite listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Il Pull-to-Refresh viene bloccato automaticamente quando l'elenco non è in posizione superiore — il conflitto con lo scorrimento è escluso architetturalmente.

Cos'è il Pull-to-Refresh?

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.

Pull-to-Refresh su iOS: UIRefreshControl

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.

Pull-to-Refresh su Android: SwipeRefreshLayout

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.

Material Pull-to-Refresh (Android 12+)

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.

Migliori pratiche ed errori comuni

Il Pull-to-Refresh è un pattern semplice da implementare, ma contiene diversi errori tipici che degradano l'esperienza utente. Esaminiamoli e come evitarli.

  • Doppio aggiornamento — l'utente può trascinare l'elenco più volte prima del completamento del caricamento. Soluzione: impostare un flag isRefreshing all'avvio e verificarlo in onRefresh(). Su iOS, endRefreshing() viene chiamato solo al termine; il blocco del gesto in UIRefreshControl è integrato.
  • Mancanza di feedback — l'indicatore di caricamento dovrebbe apparire solo dopo che l'utente ha superato la soglia. Non mostrare l'indicatore immediatamente al tocco — questo confonde gli utenti. iOS e Android lo fanno automaticamente.
  • Ignorare la durata dell'aggiornamento — se i dati si aggiornano in 200 ms, l'indicatore dovrebbe essere mostrato per almeno 500 ms in modo che l'utente noti l'aggiornamento. UIRefreshControl ha una durata minima di animazione; in Android, utilizzare Handler.postDelayed per un tempo di visualizzazione minimo.
  • Conflitto con la tastiera — con la tastiera aperta, il Pull-to-Refresh può attivarsi accidentalmente. Nascondere la tastiera all'inizio del gesto tramite view.endEditing(true) su iOS e InputMethodManager.hideSoftInputFromWindow() su Android.
  • Utilizzo per scopi diversi dall'aggiornamento — non utilizzare il Pull-to-Refresh per la navigazione (cambio di schede, tornare indietro). Questo viola le HIG di entrambe le piattaforme e disorienta gli utenti.

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.

Esempi di codice in Swift e Kotlin

Esempio 1: UIRefreshControl su iOS (Swift)

Aggiunge Pull-to-Refresh a UITableViewController con colore dello spinner personalizzato e titolo attribuito. Dopo il caricamento dei dati, l'indicatore viene nascosto.

swift
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.

Esempio 2: SwipeRefreshLayout su Android (Kotlin)

Avvolge RecyclerView in SwipeRefreshLayout con colori dell'indicatore personalizzati. onRefresh avvia il caricamento e nasconde l'indicatore al termine.

kotlin
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.

Esempio 3: SwiftUI .refreshable (iOS 15+)

SwiftUI moderno fornisce il modificatore .refreshable che aggiunge automaticamente Pull-to-Refresh a List o ScrollView.

swift
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

Il Pull-to-Refresh funziona in SwiftUI?

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.

Come prevenire il doppio aggiornamento?

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.

Il Pull-to-Refresh entra in conflitto con lo scorrimento dell'elenco?

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

  • Il Pull-to-Refresh è un pattern di aggiornamento dati tramite gesto di trascinamento verso il basso, standardizzato da Apple e Google su tutte le piattaforme mobili.
  • UIRefreshControl su iOS — un controllo con target-action, tintColor, attributedTitle e endRefreshing() obbligatorio.
  • SwipeRefreshLayout su Android — un contenitore ViewGroup con setOnRefreshListener, setColorSchemeColors e isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — una nuova API con animazione spring, raccomandata per i nuovi progetti.
  • SwiftUI .refreshable — un modificatore dichiarativo con closure asincrona, disponibile da iOS 15.
  • Il flag isRefreshing previene il doppio aggiornamento — obbligatorio su entrambe le piattaforme.
  • Il Pull-to-Refresh non è destinato alla navigazione — solo all'aggiornamento dei contenuti secondo Material Design e Apple HIG.

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.

Discuti il progetto

Leggi anche