Pull-to-Refresh: podstawy, RefreshControl i UIRefreshControl

Autor: IT Sectr Opublikowano: 2026-02-27 Czas czytania: 8 min
Pull-to-Refresh — wzorzec interfejsu mobilnego, w którym użytkownik przeciąga listę w dół palcem, inicjując ładowanie świeżych danych. Gestowi towarzyszy wizualny wskaźnik — obracający się spinner lub animowana ikona — który znika po zakończeniu ładowania. Według analizy UX Apple HIG, Pull-to-Refresh stał się standardowym mechanizmem odświeżania treści w kanałach informacyjnych, mediach społecznościowych i klientach pocztowych od momentu wdrożenia w Tweetie (2008) i późniejszej standaryzacji przez Apple i Google.

Najważniejsze

  • Pull-to-Refresh — gest przeciągnięcia listy w dół w celu odświeżenia danych, któremu towarzyszy wizualny wskaźnik ładowania.
  • W iOS używany jest UIRefreshControl (iOS 6+), dodawany do UITableViewController lub UIScrollView przez właściwość refreshControl.
  • W Android używany jest SwipeRefreshLayout (z Support Library) — ViewGroup-opakowanie dla RecyclerView lub NestedScrollView.
  • Oba API obsługują dostosowywanie kolorów, wskaźników i wywołań zwrotnych przez listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Pull-to-Refresh automatycznie blokuje się, gdy lista nie jest w górnej pozycji — konflikt z przewijaniem jest wykluczony architektonicznie.

Co to jest Pull-to-Refresh?

Pull-to-Refresh — wzorzec interfejsu użytkownika, w którym użytkownik przeciąga (pull down) listę lub obszar przewijany w dół w celu odświeżenia zawartości. Wizualnie gestowi towarzyszy wskaźnik ładowania (spinner), który pojawia się w górnej części ekranu i znika po otrzymaniu danych. Wzorzec został spopularyzowany przez aplikację Tweetie dla iPhone (2008), a następnie zestandaryzowany przez Apple (iOS 6 — UIRefreshControl) i Google (Android Support Library — SwipeRefreshLayout).

Z technicznego punktu widzenia Pull-to-Refresh to kombinacja przesuwania (śledzenie przesunięcia palca) i wyzwalacza po osiągnięciu progu. Użytkownik przeciąga listę w dół, pokonując opór (rezystywny overscroll), a po przekroczeniu progu (~80px w iOS, ~64dp w Android) uruchamiana jest animacja wskaźnika i asynchroniczne ładowanie. Jeśli użytkownik puści palec przed progiem — lista wraca do pozycji wyjściowej bez odświeżania.

Według Material Design Guidelines Pull-to-Refresh nie powinien być używany do nawigacji lub przełączania zakładek — jego jedynym przeznaczeniem jest odświeżanie danych. W IT Sectr stosujemy Pull-to-Refresh w kanałach informacyjnych, listach zamówień i czatach, gdzie świeżość danych jest kluczowa dla doświadczenia użytkownika.

Pull-to-Refresh w iOS: UIRefreshControl

UIRefreshControl — standardowy element sterujący iOS dla Pull-to-Refresh, dostępny od iOS 6. UIRefreshControl jest dodawany do UITableViewController przez właściwość refreshControl (iOS 10+) lub jako subview tabeli we wcześniejszych wersjach. Zawiera wbudowany spinner z konfigurowalnym kolorem (tintColor), atrybutem title i atrybutowanym stringiem z etykietą (np. „Odświeżanie...").

UIRefreshControl działa przez mechanizm target-action: po aktywacji gestu wywoływana jest wskazana metoda (np. refresh(_:)). Wewnątrz metody wykonywane jest asynchroniczne ładowanie danych. Po zakończeniu wywoływane jest endRefreshing(), które ukrywa wskaźnik z animacją. UIRefreshControl automatycznie zarządza czułością gestu — uruchamia się tylko w górnym położeniu tabeli (contentOffset.y <= 0).

Właściwość tintColor ustawia kolor spinnera. Atrybuty title pozwalają wyświetlać tekst „Odświeżono 2 minuty temu" po zakończeniu. Od iOS 10, UIRefreshControl obsługuje niestandardowe animacje przez UIActivityIndicatorView lub trwałe niestandardowe widoki. W IT Sectr dostosowujemy tintColor do marki i wyświetlamy czas ostatniego odświeżenia przez attributedTitle — zwiększa to zaufanie użytkowników do danych.

Pull-to-Refresh w Android: SwipeRefreshLayout

SwipeRefreshLayout — ViewGroup z Android Support Library (androidx.swiperefreshlayout), otaczająca przewijalną treść (RecyclerView, NestedScrollView, ListView) i dodająca funkcjonalność Pull-to-Refresh. W przeciwieństwie do UIRefreshControl (który jest kontrolką, a nie kontenerem), SwipeRefreshLayout to kontener, który przechwytuje zdarzenia dotykowe dziecka i uruchamia wskaźnik odświeżania po przekroczeniu progu.

SwipeRefreshLayout używa okrągłego wskaźnika postępu Material Design z konfiguracją kolorów przez setColorSchemeColors(). Metoda setOnRefreshListener ustawia callback onRefresh(), w którym wykonywane jest asynchroniczne ładowanie. Po zakończeniu wywoływane jest setRefreshing(false) w celu ukrycia wskaźnika. Ważne: setRefreshing(true) wywołuje onRefresh() ponownie — dlatego do programowego uruchomienia odświeżania użyj flagi lub metody post.

Właściwość setProgressBackgroundColorSchemeResource zmienia tło wskaźnika. setSize(SwipeRefreshLayout.LARGE) — rozmiar spinnera. W XML-układzie SwipeRefreshLayout otacza RecyclerView: swipe_refresh_layout → recycler_view. Według Google I/O 2024, SwipeRefreshLayout jest używany w 85% aplikacji Android z kanałami treści. W IT Sectr otaczamy w SwipeRefreshLayout wszystkie ekrany z asynchronicznie ładowanymi listami — zapewnia to jednolity UX na wszystkich wersjach Android.

Material Pull-to-Refresh (Android 12+)

Od Android 12 (Material You), Google zaleca używanie nowego Material Pull-to-Refresh z biblioteki material-1.6.0+ (androidx.compose.material3.pulltorefresh dla Compose). Nowe API używa animowanego wskaźnika z obsługą spring-animacji i adaptacyjnego koloru na podstawie tapety. SwipeRefreshLayout pozostaje kompatybilny dla wersji poniżej Android 12.

Najlepsze praktyki i częste błędy

Pull-to-Refresh — prosty w implementacji wzorzec, ale zawiera kilka typowych błędów, obniżających UX. Omówmy je i sposoby zapobiegania.

  • Podwójne odświeżanie — użytkownik może szarpnąć listę kilka razy przed zakończeniem ładowania. Rozwiązanie: ustaw flagę isRefreshing na starcie i sprawdzaj ją w onRefresh(). W iOS endRefreshing() wywoływane jest dopiero po zakończeniu; blokada gestu w UIRefreshControl jest wbudowana.
  • Brak informacji zwrotnej — wskaźnik ładowania powinien pojawiać się dokładnie po przekroczeniu progu przez użytkownika. Nie pokazuj wskaźnika od razu po dotknięciu — to dezorientuje. iOS i Android robią to automatycznie.
  • Ignorowanie czasu odświeżania — jeśli dane odświeżają się w 200 ms, wskaźnik powinien być widoczny przez co najmniej 500 ms, aby użytkownik zauważył odświeżenie. UIRefreshControl ma minimalny czas animacji; w Android użyj Handler.postDelayed dla minimalnego czasu wyświetlania.
  • Konflikt z klawiaturą — przy otwartej klawiaturze Pull-to-Refresh może uruchomić się przypadkowo. Ukrywaj klawiaturę na początku gestu przez view.endEditing(true) w iOS i InputMethodManager.hideSoftInputFromWindow() w Android.
  • Używanie nie do odświeżania — nie używaj Pull-to-Refresh do nawigacji (przełączanie zakładek, powrót). Narusza to HIG obu platform i dezorientuje użytkowników.

W IT Sectr dodaliśmy sprawdzanie isRefreshing w każdym projekcie po odkryciu zduplikowanych żądań w logach serwera testowego — okazało się, że użytkownicy z szybkimi palcami uruchamiali odświeżanie do 3 razy z rzędu.

Przykłady kodu w Swift i Kotlin

Przykład 1: UIRefreshControl w iOS (Swift)

Dodaje Pull-to-Refresh do UITableViewController z niestandardowym kolorem spinnera i attributed title. Po załadowaniu danych wskaźnik jest ukrywany.

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: “Przeciągnij, aby odświeżyć”
        )
        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()
        }
    }
}

Właściwość tableView.refreshControl (iOS 10+) ustawia UIRefreshControl. addTarget ze zdarzeniem .valueChanged uruchamia się przy aktywacji gestu. endRefreshing() jest obowiązkowy — bez niego wskaźnik będzie kręcił się w nieskończoność. Asynchroniczne ładowanie jest symulowane przez DispatchQueue.main.asyncAfter — w rzeczywistym projekcie byłoby to URLSession lub async/await.

Przykład 2: SwipeRefreshLayout w Android (Kotlin)

Otacza RecyclerView w SwipeRefreshLayout z niestandardowymi kolorami wskaźnika. onRefresh uruchamia ładowanie i ukrywa wskaźnik po zakończeniu.

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 ustawia kolory obracającego się wskaźnika Material Design. isRefreshing = false jest obowiązkowo wywoływane w finally, aby ukryć wskaźnik nawet przy błędzie ładowania. ViewModelScope.launch wykonuje coroutine w cyklu życia fragmentu — przy zniszczeniu fragmentu coroutine jest automatycznie anulowana, zapobiegając wyciekowi pamięci.

Przykład 3: SwiftUI .refreshable (iOS 15+)

Nowoczesny SwiftUI udostępnia modyfikator .refreshable, który automatycznie dodaje Pull-to-Refresh do List lub 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()
        }
    }
}

Modyfikator .refreshable przyjmuje async-closure, wykonywany przy Pull-to-Refresh. SwiftUI automatycznie pokazuje i ukrywa wskaźnik odświeżania, zarządza wyścigiem stanu (nie uruchamia ponownego ładowania przed zakończeniem bieżącego) i dostosowuje animację do platformy. Dla iOS 15+ jest to preferowany sposób implementacji Pull-to-Refresh w SwiftUI.

Często zadawane pytania

Pull-to-Refresh działa w SwiftUI?

Tak, SwiftUI udostępnia modyfikator .refreshable dla List lub ScrollView, dostępny od iOS 15. Wewnątrz closure wykonywany jest async-kod ładowania danych. SwiftUI automatycznie zarządza wskaźnikiem odświeżania i blokuje ponowne uruchomienia do zakończenia bieżącego ładowania — to standardowe recommended podejście dla nowych projektów.

Jak zapobiec podwójnemu odświeżaniu?

Użyj flagi isRefreshing: ustaw true na starcie ładowania i false po zakończeniu. W iOS UIRefreshControl automatycznie blokuje ponowny wywołanie, dopóki nie zostanie wywołane endRefreshing(). W Android sprawdzaj SwipeRefreshLayout.isRefreshing na początku onRefresh(): jeśli true — return. Gwarantuje to jeden żądanie na jeden gest.

Czy Pull-to-Refresh koliduje z przewijaniem listy?

UIRefreshControl i SwipeRefreshLayout uruchamiają się tylko w górnym położeniu listy (contentOffset == 0). Architektura wyklucza konflikt: dopóki lista jest przewinięta choćby o 1px, gest Pull-to-Refresh nie aktywuje się. Jeśli konflikt wystąpi — sprawdź nestedScrollingEnabled w Android lub obecność niestandardowych GestureRecognizer przechwytujących dotyk.

Podsumowanie

  • Pull-to-Refresh — wzorzec odświeżania danych przez przeciągnięcie listy w dół, zestandaryzowany przez Apple i Google na wszystkich platformach mobilnych.
  • UIRefreshControl w iOS — kontrolka z target-action, tintColor, attributedTitle i obowiązkowym endRefreshing().
  • SwipeRefreshLayout w Android — ViewGroup-kontener z setOnRefreshListener, setColorSchemeColors i isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — nowe API ze spring-animacją, zalecane dla nowych projektów.
  • SwiftUI .refreshable — deklaratywny modyfikator z async-zamykaniem, dostępny od iOS 15.
  • Flaga isRefreshing zapobiega podwójnemu odświeżaniu — obowiązkowa na obu platformach.
  • Pull-to-Refresh nie jest przeznaczony do nawigacji — tylko do odświeżania treści zgodnie z Material Design i Apple HIG.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również