Points clés
Pull-to-Refresh est un modèle d'interface utilisateur où l'utilisateur tire (pull down) une liste ou une zone défilable vers le bas pour actualiser le contenu. Visuellement, le geste est accompagné d'un indicateur de chargement (spinner) qui apparaît en haut de l'écran et disparaît après réception des données. Le modèle a été popularisé par l'application Tweetie pour iPhone (2008) et ensuite standardisé par Apple (iOS 6 — UIRefreshControl) et Google (Android Support Library — SwipeRefreshLayout).
D'un point de vue technique, le Pull-to-Refresh est une combinaison de panoramique (suivi du déplacement du doigt) et d'un déclencheur lors de l'atteinte d'un seuil. L'utilisateur tire la liste vers le bas, surmontant une résistance (overscroll résistif), et après avoir dépassé le seuil (~80px sur iOS, ~64dp sur Android), l'animation de l'indicateur et le chargement asynchrone commencent. Si l'utilisateur relâche le doigt avant le seuil, la liste revient à sa position d'origine sans actualisation.
Selon les Material Design Guidelines, le Pull-to-Refresh ne doit pas être utilisé pour la navigation ou le changement d'onglets — son seul but est l'actualisation des données. Chez IT Sectr, nous utilisons le Pull-to-Refresh dans les fils d'actualité, les listes de commandes et les chats où la fraîcheur des données est critique pour l'expérience utilisateur.
UIRefreshControl est le contrôle standard iOS pour le Pull-to-Refresh, disponible depuis iOS 6. UIRefreshControl est ajouté à UITableViewController via la propriété refreshControl (iOS 10+) ou comme sous-vue du tableau dans les versions antérieures. Il comprend un spinner intégré avec une couleur personnalisable (tintColor), un attribut title et une chaîne attribuée avec une étiquette (par exemple, "Mise à jour...").
UIRefreshControl fonctionne via le mécanisme target-action : lorsque le geste est activé, la méthode spécifiée est appelée (par exemple, refresh(_:)). À l'intérieur de la méthode, le chargement asynchrone des données est effectué. Après l'achèvement, endRefreshing() est appelé, ce qui masque l'indicateur avec une animation. UIRefreshControl gère automatiquement la sensibilité du geste — il ne se déclenche que lorsque le tableau est en position haute (contentOffset.y <= 0).
La propriété tintColor définit la couleur du spinner. attributedTitle permet d'afficher du texte comme "Mis à jour il y a 2 minutes" après l'achèvement. Depuis iOS 10, UIRefreshControl prend en charge les animations personnalisées via UIActivityIndicatorView ou des vues personnalisées persistantes. Chez IT Sectr, nous configurons tintColor selon la marque et affichons l'heure de la dernière mise à jour via attributedTitle — cela augmente la confiance des utilisateurs dans les données.
SwipeRefreshLayout est un ViewGroup de la Android Support Library (androidx.swiperefreshlayout) qui encapsule le contenu défilable (RecyclerView, NestedScrollView, ListView) et ajoute la fonctionnalité Pull-to-Refresh. Contrairement à UIRefreshControl (qui est un contrôle, pas un conteneur), SwipeRefreshLayout est un conteneur qui intercepte les événements tactiles de l'enfant et déclenche l'indicateur d'actualisation lorsque le seuil est dépassé.
SwipeRefreshLayout utilise un indicateur de progression circulaire Material Design avec personnalisation des couleurs via setColorSchemeColors(). La méthode setOnRefreshListener définit le callback onRefresh(), dans lequel le chargement asynchrone est effectué. Après l'achèvement, setRefreshing(false) est appelé pour masquer l'indicateur. Important : setRefreshing(true) appelle à nouveau onRefresh() — donc pour lancer l'actualisation par programmation, utilisez un flag ou une méthode post.
La propriété setProgressBackgroundColorSchemeResource modifie l'arrière-plan de l'indicateur. setSize(SwipeRefreshLayout.LARGE) définit la taille du spinner. Dans la mise en page XML, SwipeRefreshLayout encapsule RecyclerView : swipe_refresh_layout → recycler_view. Selon Google I/O 2024, SwipeRefreshLayout est utilisé dans 85 % des applications Android avec des fils de contenu. Chez IT Sectr, nous encapsulons tous les écrans avec des listes chargées de manière asynchrone dans SwipeRefreshLayout — cela offre une expérience utilisateur cohérente sur toutes les versions d'Android.
À partir d'Android 12 (Material You), Google recommande d'utiliser le nouveau Material Pull-to-Refresh de la bibliothèque material-1.6.0+ (androidx.compose.material3.pulltorefresh pour Compose). La nouvelle API utilise un indicateur animé avec prise en charge de l'animation spring et une couleur adaptative basée sur le fond d'écran. SwipeRefreshLayout reste compatible pour les versions antérieures à Android 12.
Le Pull-to-Refresh est un modèle simple à implémenter, mais il contient plusieurs erreurs typiques qui dégradent l'expérience utilisateur. Examinons-les et comment les éviter.
Chez IT Sectr, nous avons ajouté la vérification isRefreshing dans chaque projet après avoir découvert des requêtes en double dans les logs du serveur de test — il s'est avéré que les utilisateurs aux doigts rapides déclenchaient l'actualisation jusqu'à 3 fois de suite.
Ajoute le Pull-to-Refresh à UITableViewController avec une couleur de spinner personnalisée et un titre attribué. Après le chargement des données, l'indicateur est masqué.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: "Tirez pour actualiser"
)
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 propriété tableView.refreshControl (iOS 10+) définit UIRefreshControl. addTarget avec l'événement .valueChanged se déclenche lorsque le geste est activé. endRefreshing() est obligatoire — sans lui, l'indicateur tourne indéfiniment. Le chargement asynchrone est simulé avec DispatchQueue.main.asyncAfter — dans un projet réel, utilisez URLSession ou async/await.
Encapsule RecyclerView dans SwipeRefreshLayout avec des couleurs d'indicateur personnalisées. onRefresh lance le chargement et masque l'indicateur après l'achèvement.
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 définit les couleurs de l'indicateur rotatif Material Design. isRefreshing = false est appelé dans le bloc finally pour masquer l'indicateur même en cas d'erreur de chargement. ViewModelScope.launch exécute une coroutine dans le cycle de vie du fragment — lorsque le fragment est détruit, la coroutine est automatiquement annulée, évitant les fuites de mémoire.
SwiftUI moderne fournit le modificateur .refreshable qui ajoute automatiquement le Pull-to-Refresh à 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()
}
}
}
Le modificateur .refreshable prend une closure asynchrone qui s'exécute lors du Pull-to-Refresh. SwiftUI affiche et masque automatiquement l'indicateur d'actualisation, gère les conditions de course (ne lance pas une nouvelle actualisation tant que l'actuelle n'est pas terminée) et adapte l'animation à la plateforme. Pour iOS 15+, c'est la méthode préférée pour implémenter le Pull-to-Refresh dans SwiftUI.
Foire aux questions
Oui, SwiftUI fournit le modificateur .refreshable pour List ou ScrollView, disponible depuis iOS 15. À l'intérieur de la closure, le code asynchrone de chargement de données est exécuté. SwiftUI gère automatiquement l'indicateur d'actualisation et bloque les déclenchements répétés jusqu'à la fin du chargement en cours — c'est l'approche standard recommandée pour les nouveaux projets.
Utilisez un flag isRefreshing : définissez-le à true au début du chargement et à false après l'achèvement. Sur iOS, UIRefreshControl bloque automatiquement les appels répétés jusqu'à ce que endRefreshing() soit appelé. Sur Android, vérifiez SwipeRefreshLayout.isRefreshing au début de onRefresh() : si true — return. Cela garantit une requête par geste.
UIRefreshControl et SwipeRefreshLayout ne se déclenchent que lorsque la liste est en position haute (contentOffset == 0). L'architecture élimine le conflit : tant que la liste est défilée ne serait-ce que de 1px, le geste Pull-to-Refresh ne s'active pas. Si un conflit survient, vérifiez nestedScrollingEnabled sur Android ou la présence de GestureRecognizers personnalisés interceptant les touches.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi