Principalele puncte
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.
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.
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.
Î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.
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.
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.
Adaugă Pull-to-Refresh în UITableViewController cu culoare personalizată a spinnerului și attributed title. După încărcarea datelor, indicatorul este ascuns.
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.
Înfășoară RecyclerView în SwipeRefreshLayout cu culori personalizate ale indicatorului. onRefresh pornește încărcarea și ascunde indicatorul după finalizare.
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.
SwiftUI modern oferă modificatorul .refreshable, care adaugă automat Pull-to-Refresh la List sau 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()
}
}
}
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
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.
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.
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
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.
Citiți și