Pull-to-Refresh: основи, RefreshControl и UIRefreshControl

Автор: IT Sectr Публикувано: 2026-02-27 Време за четене: 8 мин
Pull-to-Refresh — модел на мобилен интерфейс, при който потребителят дърпа списъка надолу с пръст, инициирайки зареждане на свежи данни. Жестът е придружен от визуален индикатор — въртящ се спинер или анимирана икона, — който изчезва след завършване на зареждането. Според UX анализа на Apple HIG, Pull-to-Refresh се превърна в стандартен механизъм за обновяване на съдържание в новинарски емисии, социални мрежи и имейл клиенти от внедряването в Tweetie (2008) и последващата стандартизация от Apple и Google.

Основни точки

  • Pull-to-Refresh — жест на дърпане на списъка надолу за обновяване на данни, придружен от визуален индикатор за зареждане.
  • В iOS се използва UIRefreshControl (iOS 6+), добавян към UITableViewController или UIScrollView чрез свойството refreshControl.
  • В Android се използва SwipeRefreshLayout (от Support Library) — ViewGroup-обвивка за RecyclerView или NestedScrollView.
  • И двата API поддържат персонализиране на цветове, индикатори и обратни извиквания чрез listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Pull-to-Refresh автоматично се блокира, когато списъкът не е в горна позиция — конфликтът с превъртане е изключен архитектурно.

Какво е Pull-to-Refresh?

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 в новинарски емисии, списъци с поръчки и чатове, където свежестта на данните е критична за потребителското изживяване.

Pull-to-Refresh в iOS: UIRefreshControl

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 — това повишава доверието на потребителите в данните.

Pull-to-Refresh в Android: SwipeRefreshLayout

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.

Material Pull-to-Refresh (Android 12+)

От 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. Нека ги разгледаме и начините за предотвратяване.

  • Двойно обновяване — потребителят може да дръпне списъка няколко пъти преди завършване на зареждането. Решение: задайте флаг isRefreshing при стартиране и го проверявайте в onRefresh(). В iOS endRefreshing() се извиква само след завършване; блокирането на жеста в UIRefreshControl е вградено.
  • Липса на обратна връзка — индикаторът за зареждане трябва да се появи след като потребителят превиши прага. Не показвайте индикатора веднага при докосване — това обърква. iOS и Android правят това автоматично.
  • Игнориране на времето за обновяване — ако данните се обновят за 200 ms, индикаторът трябва да се показва поне 500 ms, за да забележи потребителят обновяването. UIRefreshControl има минимално време за анимация; в Android използвайте Handler.postDelayed за минимално време на показване.
  • Конфликт с клавиатурата — при отворена клавиатура Pull-to-Refresh може случайно да се активира. Скрийте клавиатурата в началото на жеста чрез view.endEditing(true) в iOS и InputMethodManager.hideSoftInputFromWindow() в Android.
  • Използване не за обновяване — не използвайте Pull-to-Refresh за навигация (превключване на раздели, връщане назад). Това нарушава HIG и на двете платформи и дезориентира потребителите.

В IT Sectr добавихме проверка на isRefreshing във всеки проект, след като открихме дублиращи се заявки в логовете на тестовия сървър — оказа се, че потребители с бързи пръсти стартираха обновяване до 3 пъти подред.

Примери за код в Swift и Kotlin

Пример 1: UIRefreshControl в iOS (Swift)

Добавя Pull-to-Refresh в UITableViewController с персонализиран цвят на спинера и attributed title. След зареждане на данните индикаторът се скрива.

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: “Дръпнете за обновяване”
        )
        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.

Пример 2: SwipeRefreshLayout в Android (Kotlin)

Обвива RecyclerView в SwipeRefreshLayout с персонализирани цветове на индикатора. onRefresh стартира зареждане и скрива индикатора след завършване.

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 задава цветовете на въртящия се индикатор на Material Design. isRefreshing = false задължително се извиква в finally, за да се скрие индикатора дори при грешка при зареждане. ViewModelScope.launch изпълнява корутина в жизнения цикъл на фрагмента — при унищожаване на фрагмента корутината автоматично се отменя, предотвратявайки изтичане на памет.

Пример 3: SwiftUI .refreshable (iOS 15+)

Модерният SwiftUI предоставя модификатора .refreshable, който автоматично добавя Pull-to-Refresh към List или 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()
        }
    }
}

Модификаторът .refreshable приема async-closure, който се изпълнява при Pull-to-Refresh. SwiftUI автоматично показва и скрива индикатора за обновяване, управлява състезателни условия (не стартира повторно зареждане до завършване на текущото) и адаптира анимацията към платформата. За iOS 15+ това е preferred начинът за имплементиране на Pull-to-Refresh в SwiftUI.

Често задавани въпроси

Работи ли 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. Това гарантира една заявка на жест.

Конфликтира ли Pull-to-Refresh с превъртането на списъка?

UIRefreshControl и SwipeRefreshLayout се активират само в горна позиция на списъка (contentOffset == 0). Архитектурата изключва конфликт: докато списъкът е превъртян дори с 1px, жестът Pull-to-Refresh не се активира. Ако възникне конфликт — проверете nestedScrollingEnabled в Android или наличието на персонализирани GestureRecognizer, прихващащи докосвания.

Резюме

  • Pull-to-Refresh — модел за обновяване на данни чрез дърпане на списъка надолу, стандартизиран от Apple и Google на всички мобилни платформи.
  • UIRefreshControl в iOS — контролен елемент с target-action, tintColor, attributedTitle и задължителен endRefreshing().
  • SwipeRefreshLayout в Android — ViewGroup контейнер с setOnRefreshListener, setColorSchemeColors и isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — нов API с spring анимация, препоръчителен за нови проекти.
  • SwiftUI .refreshable — декларативен модификатор с async-closure, достъпен от iOS 15.
  • Флагът isRefreshing предотвратява двойно обновяване — задължителен и на двете платформи.
  • Pull-to-Refresh не е предназначен за навигация — само за обновяване на съдържание според Material Design и Apple HIG.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също