Главне тачке
Pull-to-Refresh — образац корисничког интерфејса у којем корисник повлачи (pull down) листу или област за померање надоле ради ажурирања садржаја. Визуелно, покрет је праћен индикатором учитавања (spinner) који се појављује на врху екрана и нестаје након пријема података. Овај образац је популаризовала апликација Tweetie за iPhone (2008), а касније су га стандардизовали Apple (iOS 6 — UIRefreshControl) и Google (Android Support Library — SwipeRefreshLayout).
Са техничке тачке гледишта, Pull-to-Refresh је комбинација панорамирања (праћење померања прста) и окидача при достизању прага. Корисник повлачи листу надоле, савладавајући отпор (резистивни overscroll), и након преласка прага (~80px у iOS, ~64dp у Android) покреће се анимација индикатора и асинхроно учитавање. Ако корисник отпусти прст пре прага — листа се враћа у почетни положај без ажурирања.
Према Material Design Guidelines, Pull-to-Refresh не треба користити за навигацију или пребацивање картица — његова једина намена је ажурирање података. У IT Sectr користимо Pull-to-Refresh у News фидовима, листама поруџбина и четовима, где је свежина података критична за корисничко искуство.
UIRefreshControl — стандардни iOS контролни елемент за Pull-to-Refresh, доступан од iOS 6. UIRefreshControl се додаје у UITableViewController преко својства refreshControl (iOS 10+) или као subview табеле у старијим верзијама. Садржи уграђени спинер са подесивом бојом (tintColor), атрибутом title и атрибуираним стрингом са натписом (нпр. „Ажурирање...").
UIRefreshControl ради преко механизма target-action: при активацији покрета позива се наведени метод (нпр. refresh(_:)). Унутар метода се извршава асинхроно учитавање података. Након завршетка позива се endRefreshing(), који сакрива индикатор уз анимацију. UIRefreshControl аутоматски управља осетљивошћу покрета — активира се само у горњем положају табеле (contentOffset.y <= 0).
Својство tintColor поставља боју спинерa. Атрибути title омогућавају приказ текста „Ажурирано пре 2 минута" након завршетка. Од iOS 10, UIRefreshControl подржава прилагођене анимације преко UIActivityIndicatorView или перзистентне прилагођене приказе. У IT Sectr прилагођавамо tintColor бренду и приказујемо време последњег ажурирања кроз attributedTitle — то повећава поверење корисника у податке.
SwipeRefreshLayout — ViewGroup из Android Support Library (androidx.swiperefreshlayout) која обавија садржај који се помера (RecyclerView, NestedScrollView, ListView) и додаје функционалност Pull-to-Refresh. За разлику од UIRefreshControl (који је контрола, а не контејнер), SwipeRefreshLayout је контејнер који пресреће додирне догађаје детета и покреће индикатор ажурирања при преласку прага.
SwipeRefreshLayout користи кружни индикатор напретка Material Design-а са подешавањем боје кроз setColorSchemeColors(). Метода setOnRefreshListener поставља повратни позив onRefresh(), у којем се извршава асинхроно учитавање. Након завршетка позива се setRefreshing(false) ради сакривања индикатора. Важно: setRefreshing(true) поново позива onRefresh() — зато за програмско покретање ажурирања користите flag или post метод.
Својство setProgressBackgroundColorSchemeResource мења позадину индикатора. setSize(SwipeRefreshLayout.LARGE) — величина спинерa. У XML распореду, SwipeRefreshLayout обавија RecyclerView: swipe_refresh_layout → recycler_view. Према Google I/O 2024, SwipeRefreshLayout се користи у 85% Android апликација са токовима садржаја. У IT Sectr обавијамо у SwipeRefreshLayout све екране са асинхроно учитаваним листама — то обезбеђује јединствен UX на свим верзијама Android-а.
Од Android 12 (Material You), Google препоручује коришћење новог Material Pull-to-Refresh из библиотеке material-1.6.0+ (androidx.compose.material3.pulltorefresh за Compose). Нови API користи анимирани индикатор са подршком за spring анимацију и адаптивну боју на основу позадине. SwipeRefreshLayout остаје компатибилан за верзије испод Android 12.
Pull-to-Refresh — образац који је једноставан за имплементацију, али садржи неколико типичних грешака које смањују UX. Размотримо их и начине превенције.
У IT Sectr смо додали проверу isRefreshing у сваки пројекат након што смо открили дуплиране захтеве у логовима тест сервера — испоставило се да су корисници са брзим прстима покретали ажурирање до 3 пута узастопно.
Додаје Pull-to-Refresh у UITableViewController са прилагођеном бојом спинерa и attributed title. Након учитавања података, индикатор се сакрива.
import UIKit
class FeedTableViewController: UITableViewController {
private var items: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.refreshControl = UIRefreshControl()
refreshControl?.tintColor = .systemBlue
refreshControl?.attributedTitle = NSAttributedString(
string: “Повуците за ажурирање”
)
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()
}
}
}
Својство tableView.refreshControl (iOS 10+) поставља UIRefreshControl. addTarget са догађајем .valueChanged се активира при покретању геста. endRefreshing() је обавезан — без њега индикатор ће се вртети бесконачно. Асинхроно учитавање је симулирано кроз DispatchQueue.main.asyncAfter — у правом пројекту би било URLSession или async/await.
Обавија RecyclerView у SwipeRefreshLayout са прилагођеним бојама индикатора. onRefresh покреће учитавање и сакрива индикатор након завршетка.
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 поставља боје ротирајућег индикатора Material Design-а. isRefreshing = false се обавезно позива у finally да би се индикатор сакрио чак и при грешци учитавања. ViewModelScope.launch извршава корутину у животном циклусу фрагмента — при уништењу фрагмента, корутина се аутоматски отказује, спречавајући цурење меморије.
Модерни SwiftUI пружа модификатор .refreshable, који аутоматски додаје Pull-to-Refresh у List или 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()
}
}
}
Модификатор .refreshable прима async-closure који се извршава при Pull-to-Refresh. SwiftUI аутоматски приказује и сакрива индикатор ажурирања, управља трком стања (не покреће поновно учитавање док се текуће не заврши) и прилагођава анимацију платформи. За iOS 15+ ово је preferred начин имплементације Pull-to-Refresh у SwiftUI.
Често постављана питања
Да, SwiftUI пружа модификатор .refreshable за List или ScrollView, доступан од iOS 15. Унутар closure-а се извршава async код учитавања података. SwiftUI аутоматски управља индикатором ажурирања и блокира поновна покретања до завршетка текућег учитавања — ово је стандардни recommended приступ за нове пројекте.
Користите flag isRefreshing: поставите true на почетку учитавања и false након завршетка. У iOS-у, UIRefreshControl аутоматски блокира поновни позив док endRefreshing() није позван. У Android-у проверавајте SwipeRefreshLayout.isRefreshing на почетку onRefresh(): ако је true — return. Ово гарантује један захтев по покрету.
UIRefreshControl и SwipeRefreshLayout се активирају само у горњем положају листе (contentOffset == 0). Архитектура искључује сукоб: док је листа померена макар за 1px, Pull-to-Refresh покрет се не активира. Ако дође до сукоба — проверите nestedScrollingEnabled у Android-у или присуство прилагођених GestureRecognizer-а који пресрећу додире.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође