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 використовує анімований індикатор з підтримкою spring-анімації та адаптивного кольору на основі шпалер. 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 автоматично керує індикатором оновлення та блокує повторні запуски до завершення поточного завантаження — це стандартний рекомендований підхід для нових проектів.

Як запобігти подвійному оновленню?

Використовуйте флаг 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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