Hlavní body
Pull-to-Refresh — vzor uživatelského rozhraní, při kterém uživatel táhne (pull down) seznam nebo posouvatelnou oblast dolů pro aktualizaci obsahu. Vizuálně je gesto doprovázeno indikátorem načítání (spinner), který se objeví v horní části obrazovky a zmizí po obdržení dat. Vzor byl popularizován aplikací Tweetie pro iPhone (2008) a následně standardizován Apple (iOS 6 — UIRefreshControl) a Google (Android Support Library — SwipeRefreshLayout).
Z technického hlediska je Pull-to-Refresh kombinací posouvání (sledování pohybu prstu) a spouštěče při dosažení prahu. Uživatel táhne seznam dolů, překonává odpor (rezistivní overscroll), a po překročení prahu (~80px v iOS, ~64dp v Android) se spustí animace indikátoru a asynchronní načítání. Pokud uživatel pustí prst před prahem — seznam se vrátí do výchozí pozice bez aktualizace.
Podle Material Design Guidelines by Pull-to-Refresh neměl být používán pro navigaci nebo přepínání karet — jeho jediným účelem je aktualizace dat. V IT Sectr používáme Pull-to-Refresh v novinkových feedech, seznamech objednávek a chatech, kde je čerstvost dat kritická pro uživatelský zážitek.
UIRefreshControl — standardní ovládací prvek iOS pro Pull-to-Refresh, dostupný od iOS 6. UIRefreshControl se přidává do UITableViewController přes vlastnost refreshControl (iOS 10+) nebo jako subview tabulky ve starších verzích. Obsahuje vestavěný spinner s nastavitelnou barvou (tintColor), atributem title a atributovaným řetězcem s popiskem (např. „Aktualizace...").
UIRefreshControl funguje přes mechanismus target-action: při aktivaci gesta se zavolá určená metoda (např. refresh(_:)). Uvnitř metody probíhá asynchronní načítání dat. Po dokončení se zavolá endRefreshing(), který skryje indikátor s animací. UIRefreshControl automaticky spravuje citlivost gesta — aktivuje se pouze v horní poloze tabulky (contentOffset.y <= 0).
Vlastnost tintColor nastavuje barvu spinneru. Atributy title umožňují zobrazit text „Aktualizováno před 2 minutami" po dokončení. Od iOS 10 podporuje UIRefreshControl vlastní animace přes UIActivityIndicatorView nebo trvalá vlastní zobrazení. V IT Sectr přizpůsobujeme tintColor značce a zobrazujeme čas poslední aktualizace přes attributedTitle — to zvyšuje důvěru uživatelů v data.
SwipeRefreshLayout — ViewGroup z Android Support Library (androidx.swiperefreshlayout), který obaluje posouvatelný obsah (RecyclerView, NestedScrollView, ListView) a přidává funkcionalitu Pull-to-Refresh. Na rozdíl od UIRefreshControl (který je ovládacím prvkem, nikoli kontejnerem), je SwipeRefreshLayout kontejner, který zachycuje dotykové události dítěte a spouští indikátor aktualizace při překročení prahu.
SwipeRefreshLayout používá kruhový indikátor průběhu Material Design s nastavením barev přes setColorSchemeColors(). Metoda setOnRefreshListener nastavuje zpětné volání onRefresh(), ve kterém probíhá asynchronní načítání. Po dokončení se zavolá setRefreshing(false) pro skrytí indikátoru. Důležité: setRefreshing(true) znovu volá onRefresh() — proto pro programové spuštění aktualizace použijte flag nebo post metodu.
Vlastnost setProgressBackgroundColorSchemeResource mění pozadí indikátoru. setSize(SwipeRefreshLayout.LARGE) — velikost spinneru. V XML rozvržení SwipeRefreshLayout obaluje RecyclerView: swipe_refresh_layout → recycler_view. Podle Google I/O 2024 se SwipeRefreshLayout používá v 85% Android aplikací s obsahovými feedy. V IT Sectr obalujeme všechny obrazovky s asynchronně načítanými seznamy do SwipeRefreshLayout — to zajišťuje jednotné UX na všech verzích Android.
Od Android 12 (Material You) Google doporučuje používat nový Material Pull-to-Refresh z knihovny material-1.6.0+ (androidx.compose.material3.pulltorefresh pro Compose). Nové API používá animovaný indikátor s podporou spring animace a adaptivní barvy na základě tapety. SwipeRefreshLayout zůstává kompatibilní pro verze pod Android 12.
Pull-to-Refresh — snadno implementovatelný vzor, ale obsahuje několik typických chyb, které snižují UX. Podívejme se na ně a způsoby prevence.
V IT Sectr jsme přidali kontrolu isRefreshing do každého projektu poté, co jsme objevili duplicitní požadavky v logech testovacího serveru — ukázalo se, že uživatelé s rychlými prsty spouštěli aktualizaci až 3krát za sebou.
Přidává Pull-to-Refresh do UITableViewController s vlastní barvou spinneru a attributed title. Po načtení dat je indikátor skryt.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: “Potáhněte pro aktualizaci”
)
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()
}
}
}
Vlastnost tableView.refreshControl (iOS 10+) nastavuje UIRefreshControl. addTarget s událostí .valueChanged se spouští při aktivaci gesta. endRefreshing() je povinný — bez něj by se indikátor točil donekonečna. Asynchronní načítání je simulováno přes DispatchQueue.main.asyncAfter — v reálném projektu by to bylo URLSession nebo async/await.
Obalí RecyclerView do SwipeRefreshLayout s vlastními barvami indikátoru. onRefresh spouští načítání a po dokončení skryje indikátor.
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 nastavuje barvy otáčejícího se indikátoru Material Design. isRefreshing = false se povinně volá v finally, aby se indikátor skryl i při chybě načítání. ViewModelScope.launch provádí korutinu v životním cyklu fragmentu — při zničení fragmentu je korutina automaticky zrušena, čímž se zabraňuje úniku paměti.
Moderní SwiftUI poskytuje modifikátor .refreshable, který automaticky přidává Pull-to-Refresh k List nebo 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()
}
}
}
Modifikátor .refreshable přijímá async-closure, který se provádí při Pull-to-Refresh. SwiftUI automaticky zobrazuje a skrývá indikátor aktualizace, spravuje závodní stavy (nespouští nové načítání, dokud není dokončeno aktuální) a přizpůsobuje animaci platformě. Pro iOS 15+ je to preferred způsob implementace Pull-to-Refresh ve SwiftUI.
Často kladené otázky
Ano, SwiftUI poskytuje modifikátor .refreshable pro List nebo ScrollView, dostupný od iOS 15. Uvnitř closure se provádí asynchronní kód načítání dat. SwiftUI automaticky spravuje indikátor aktualizace a blokuje opětovné spuštění do dokončení aktuálního načítání — to je standardní recommended přístup pro nové projekty.
Použijte flag isRefreshing: nastavte true při startu načítání a false po dokončení. V iOS UIRefreshControl automaticky blokuje opakované volání, dokud není voláno endRefreshing(). V Android kontrolujte SwipeRefreshLayout.isRefreshing na začátku onRefresh(): pokud true — return. To zaručuje jeden požadavek na jedno gesto.
UIRefreshControl a SwipeRefreshLayout se aktivují pouze v horní pozici seznamu (contentOffset == 0). Architektura vylučuje konflikt: dokud je seznam posunut byť jen o 1px, gesto Pull-to-Refresh se neaktivuje. Pokud konflikt nastane — zkontrolujte nestedScrollingEnabled v Android nebo přítomnost vlastních GestureRecognizer, které zachycují dotyky.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také