Основни точки
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 в новинарски емисии, списъци с поръчки и чатове, където свежестта на данните е критична за потребителското изживяване.
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 задава цвета на спинера. Атрибутите 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() — затова за програмно стартиране на обновяване използвайте флаг или post метод.
Свойството setProgressBackgroundColorSchemeResource променя фона на индикатора. setSize(SwipeRefreshLayout.LARGE) — размер на спинера. В 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 с персонализиран цвят на спинера и 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 подход за нови проекти.
Използвайте флага 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също