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 — это контейнер, который перехватывает touch-события ребёнка и запускает индикатор обновления при превышении порога.

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 использует animated-индикатор с поддержкой spring-анимации и адаптивного цвета на основе wallpaper. SwipeRefreshLayout остаётся совместимым для версий ниже Android 12.

Лучшие практики и частые ошибки

Pull-to-Refresh — простой в реализации паттерн, но содержит несколько типовых ошибок, снижающих UX. Рассмотрим их и способы предотвращения.

  • Двойное обновление — пользователь может дёрнуть список несколько раз до завершения загрузки. Решение: установите флаг isRefreshing при старте и проверяйте его в onRefresh(). В iOS endRefreshing() вызывается только после завершения; блокировка жеста в UIRefreshControl встроена.
  • Отсутствие обратной связи — индикатор загрузки должен появляться строго после того, как пользователь преодолел порог. Не показывайте индикатор сразу при касании — это сбивает с толку. iOS и Android делают это автоматически.
  • Игнорирование времени обновления — если данные обновляются за 200 мс, индикатор должен показываться не менее 500 мс, чтобы пользователь заметил обновление. 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-замыканием, доступный с iOS 15.
  • Флаг isRefreshing предотвращает двойное обновление — обязателен в обеих платформах.
  • Pull-to-Refresh не предназначен для навигации — только для обновления контента по Material Design и Apple HIG.

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

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также