Pull-to-Refresh: baze, RefreshControl și UIRefreshControl

Autor: IT Sectr Publicat: 2026-02-27 Timp de citire: 8 min
Pull-to-Refresh — un model de interfață mobilă în care utilizatorul trage lista în jos cu degetul, inițiind încărcarea datelor proaspete. Gestul este însoțit de un indicator vizual — un spinner rotitor sau o pictogramă animată — care dispare după finalizarea încărcării. Conform analizei UX Apple HIG, Pull-to-Refresh a devenit mecanismul standard de actualizare a conținutului în fluxurile de știri, rețelele sociale și clienții de e-mail de la implementarea în Tweetie (2008) și standardizarea ulterioară de către Apple și Google.

Principalele puncte

  • Pull-to-Refresh — gestul de tragere a listei în jos pentru actualizarea datelor, însoțit de un indicator vizual de încărcare.
  • În iOS se utilizează UIRefreshControl (iOS 6+), adăugat în UITableViewController sau UIScrollView prin proprietatea refreshControl.
  • În Android se utilizează SwipeRefreshLayout (din Support Library) — un ViewGroup-wrapper pentru RecyclerView sau NestedScrollView.
  • Ambele API acceptă personalizarea culorilor, indicatorilor și callback-urilor prin listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Pull-to-Refresh se blochează automat când lista nu este în poziția de sus — conflictul cu derularea este exclus arhitectural.

Ce este Pull-to-Refresh?

Pull-to-Refresh — un model de interfață cu utilizatorul în care utilizatorul trage (pull down) o listă sau o zonă derulabilă în jos pentru a actualiza conținutul. Vizual, gestul este însoțit de un indicator de încărcare (spinner) care apare în partea de sus a ecranului și dispare după primirea datelor. Modelul a fost popularizat de aplicația Tweetie pentru iPhone (2008) și ulterior standardizat de Apple (iOS 6 — UIRefreshControl) și Google (Android Support Library — SwipeRefreshLayout).

Din punct de vedere tehnic, Pull-to-Refresh este o combinație de panare (urmărirea deplasării degetului) și declanșare la atingerea pragului. Utilizatorul trage lista în jos, depășind rezistența (overscroll rezistiv), iar după depășirea pragului (~80px în iOS, ~64dp în Android) pornește animația indicatorului și încărcarea asincronă. Dacă utilizatorul eliberează degetul înainte de prag — lista revine la poziția inițială fără actualizare.

Conform Material Design Guidelines, Pull-to-Refresh nu trebuie utilizat pentru navigare sau comutare între file — singurul său scop este actualizarea datelor. La IT Sectr folosim Pull-to-Refresh în fluxurile de știri, listele de comenzi și chat-uri, unde prospețimea datelor este critică pentru experiența utilizatorului.

Pull-to-Refresh în iOS: UIRefreshControl

UIRefreshControl — elementul de control standard iOS pentru Pull-to-Refresh, disponibil din iOS 6. UIRefreshControl se adaugă în UITableViewController prin proprietatea refreshControl (iOS 10+) sau ca subview al tabelului în versiunile mai vechi. Conține un spinner încorporat cu culoare configurabilă (tintColor), atribut title și un șir atribuit cu etichetă (de exemplu, „Actualizare...").

UIRefreshControl funcționează prin mecanismul target-action: la activarea gestului, se apelează metoda indicată (de exemplu, refresh(_:)). În interiorul metodei se execută încărcarea asincronă a datelor. După finalizare, se apelează endRefreshing(), care ascunde indicatorul cu animație. UIRefreshControl gestionează automat sensibilitatea gestului — se activează doar în poziția de sus a tabelului (contentOffset.y <= 0).

Proprietatea tintColor setează culoarea spinnerului. Atributele title permit afișarea textului „Actualizat acum 2 minute" după finalizare. Începând cu iOS 10, UIRefreshControl suportă animații personalizate prin UIActivityIndicatorView sau vizualizări personalizate persistente. La IT Sectr personalizăm tintColor conform brandului și afișăm ora ultimei actualizări prin attributedTitle — aceasta crește încrederea utilizatorilor în date.

Pull-to-Refresh în Android: SwipeRefreshLayout

SwipeRefreshLayout — ViewGroup din Android Support Library (androidx.swiperefreshlayout) care înfășoară conținutul derulabil (RecyclerView, NestedScrollView, ListView) și adaugă funcționalitatea Pull-to-Refresh. Spre deosebire de UIRefreshControl (care este un control, nu un container), SwipeRefreshLayout este un container care interceptează evenimentele tactile ale copilului și pornește indicatorul de actualizare la depășirea pragului.

SwipeRefreshLayout utilizează indicatorul circular de progres Material Design cu configurarea culorii prin setColorSchemeColors(). Metoda setOnRefreshListener setează callback-ul onRefresh(), în care se execută încărcarea asincronă. După finalizare, se apelează setRefreshing(false) pentru ascunderea indicatorului. Important: setRefreshing(true) apelează din nou onRefresh() — prin urmare, pentru pornirea programatică a actualizării, utilizați un flag sau metoda post.

Proprietatea setProgressBackgroundColorSchemeResource modifică fundalul indicatorului. setSize(SwipeRefreshLayout.LARGE) — dimensiunea spinnerului. În layout-ul XML, SwipeRefreshLayout înfășoară RecyclerView: swipe_refresh_layout → recycler_view. Conform Google I/O 2024, SwipeRefreshLayout este utilizat în 85% din aplicațiile Android cu fluxuri de conținut. La IT Sectr înfășurăm în SwipeRefreshLayout toate ecranele cu liste încărcate asincron — aceasta asigură un UX uniform pe toate versiunile de Android.

Material Pull-to-Refresh (Android 12+)

Începând cu Android 12 (Material You), Google recomandă utilizarea noului Material Pull-to-Refresh din biblioteca material-1.6.0+ (androidx.compose.material3.pulltorefresh pentru Compose). Noul API utilizează un indicator animat cu suport pentru animație spring și culoare adaptivă bazată pe tapet. SwipeRefreshLayout rămâne compatibil pentru versiunile sub Android 12.

Cele mai bune practici și erori frecvente

Pull-to-Refresh — un model simplu de implementat, dar conține câteva erori tipice care reduc UX. Să le analizăm și modalitățile de prevenire.

  • Actualizare dublă — utilizatorul poate trage lista de mai multe ori înainte de finalizarea încărcării. Soluție: setați flag-ul isRefreshing la pornire și verificați-l în onRefresh(). În iOS, endRefreshing() se apelează doar după finalizare; blocarea gestului în UIRefreshControl este încorporată.
  • Lipsa feedback-ului — indicatorul de încărcare trebuie să apară strict după ce utilizatorul a depășit pragul. Nu afișați indicatorul imediat la atingere — este derutant. iOS și Android fac acest lucru automat.
  • Ignorarea timpului de actualizare — dacă datele se actualizează în 200 ms, indicatorul trebuie afișat cel puțin 500 ms pentru ca utilizatorul să observe actualizarea. UIRefreshControl are un timp minim de animație; în Android, utilizați Handler.postDelayed pentru timpul minim de afișare.
  • Conflict cu tastatura — cu tastatura deschisă, Pull-to-Refresh se poate activa accidental. Ascundeți tastatura la începutul gestului prin view.endEditing(true) în iOS și InputMethodManager.hideSoftInputFromWindow() în Android.
  • Utilizarea pentru altceva decât actualizare — nu utilizați Pull-to-Refresh pentru navigare (comutare file, revenire). Aceasta încalcă HIG-ul ambelor platforme și dezorientează utilizatorii.

La IT Sectr am adăugat verificarea isRefreshing în fiecare proiect după ce am descoperit cereri duplicate în logurile serverului de test — s-a dovedit că utilizatorii cu degete rapide porneau actualizarea de până la 3 ori consecutiv.

Exemple de cod în Swift și Kotlin

Exemplul 1: UIRefreshControl în iOS (Swift)

Adaugă Pull-to-Refresh în UITableViewController cu culoare personalizată a spinnerului și attributed title. După încărcarea datelor, indicatorul este ascuns.

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: “Trageți pentru a actualiza”
        )
        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()
        }
    }
}

Proprietatea tableView.refreshControl (iOS 10+) setează UIRefreshControl. addTarget cu evenimentul .valueChanged se declanșează la activarea gestului. endRefreshing() este obligatoriu — fără el, indicatorul se va învârti la infinit. Încărcarea asincronă este simulată prin DispatchQueue.main.asyncAfter — într-un proiect real ar fi URLSession sau async/await.

Exemplul 2: SwipeRefreshLayout în Android (Kotlin)

Înfășoară RecyclerView în SwipeRefreshLayout cu culori personalizate ale indicatorului. onRefresh pornește încărcarea și ascunde indicatorul după finalizare.

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 setează culorile indicatorului rotativ Material Design. isRefreshing = false se apelează obligatoriu în finally pentru a ascunde indicatorul chiar și la eroare de încărcare. ViewModelScope.launch execută corutina în ciclul de viață al fragmentului — la distrugerea fragmentului, corutina este anulată automat, prevenind scurgerile de memorie.

Exemplul 3: SwiftUI .refreshable (iOS 15+)

SwiftUI modern oferă modificatorul .refreshable, care adaugă automat Pull-to-Refresh la List sau 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()
        }
    }
}

Modificatorul .refreshable primește un async-closure care se execută la Pull-to-Refresh. SwiftUI afișează și ascunde automat indicatorul de actualizare, gestionează condițiile de cursă (nu pornește o nouă încărcare până la finalizarea celei curente) și adaptează animația la platformă. Pentru iOS 15+, aceasta este metoda preferred de implementare a Pull-to-Refresh în SwiftUI.

Întrebări frecvente

Pull-to-Refresh funcționează în SwiftUI?

Da, SwiftUI oferă modificatorul .refreshable pentru List sau ScrollView, disponibil din iOS 15. În interiorul closure-ului se execută codul async de încărcare a datelor. SwiftUI gestionează automat indicatorul de actualizare și blochează repornirile până la finalizarea încărcării curente — aceasta este abordarea recommended standard pentru proiecte noi.

Cum prevenim actualizarea dublă?

Utilizați flag-ul isRefreshing: setați true la pornirea încărcării și false după finalizare. În iOS, UIRefreshControl blochează automat re-apelarea până când endRefreshing() este apelat. În Android, verificați SwipeRefreshLayout.isRefreshing la începutul onRefresh(): dacă true — return. Aceasta garantează o singură cerere per gest.

Pull-to-Refresh intră în conflict cu derularea listei?

UIRefreshControl și SwipeRefreshLayout se activează doar în poziția de sus a listei (contentOffset == 0). Arhitectura exclude conflictul: cât timp lista este derulată chiar și cu 1px, gestul Pull-to-Refresh nu se activează. Dacă apare un conflict — verificați nestedScrollingEnabled în Android sau prezența unor GestureRecognizer personalizate care interceptează atingerile.

Rezumat

  • Pull-to-Refresh — model de actualizare a datelor prin tragerea listei în jos, standardizat de Apple și Google pe toate platformele mobile.
  • UIRefreshControl în iOS — control cu target-action, tintColor, attributedTitle și endRefreshing() obligatoriu.
  • SwipeRefreshLayout în Android — container ViewGroup cu setOnRefreshListener, setColorSchemeColors și isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — API nou cu animație spring, recomandat pentru proiecte noi.
  • SwiftUI .refreshable — modificator declarativ cu async-closure, disponibil din iOS 15.
  • Flag-ul isRefreshing previne actualizarea dublă — obligatoriu pe ambele platforme.
  • Pull-to-Refresh nu este destinat navigării — doar pentru actualizarea conținutului conform Material Design și Apple HIG.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și