Pull-to-Refresh: βασικά, RefreshControl και UIRefreshControl

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-27 Χρόνος ανάγνωσης: 8 λεπ
Pull-to-Refresh — ένα μοτίβο διεπαφής κινητού όπου ο χρήστης τραβάει τη λίστα προς τα κάτω με το δάχτυλο, ενεργοποιώντας τη φόρτωση νέων δεδομένων. Η κίνηση συνοδεύεται από ένα οπτικό δείκτη — περιστρεφόμενο spinner ή κινούμενο εικονίδιο — που εξαφανίζεται μετά την ολοκλήρωση της φόρτωσης. Σύμφωνα με την ανάλυση UX του Apple HIG, το Pull-to-Refresh έχει γίνει ο τυπικός μηχανισμός ενημέρωσης περιεχομένου σε ροές ειδήσεων, κοινωνικά δίκτυα και προγράμματα-πελάτες email από την εφαρμογή του στο Tweetie (2008) και την επακόλουθη τυποποίηση από την Apple και την Google.

Κύρια σημεία

  • Pull-to-Refresh — κίνηση τραβήγματος της λίστας προς τα κάτω για ενημέρωση δεδομένων, συνοδευόμενη από οπτικό δείκτη φόρτωσης.
  • Στο iOS χρησιμοποιείται το UIRefreshControl (iOS 6+), το οποίο προστίθεται στο UITableViewController ή UIScrollView μέσω της ιδιότητας refreshControl.
  • Στο Android χρησιμοποιείται το SwipeRefreshLayout (από τη Support Library) — ένα ViewGroup-wrapper για RecyclerView ή NestedScrollView.
  • Και τα δύο API υποστηρίζουν προσαρμογή χρωμάτων, δεικτών και callback μέσω 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 είναι ένας συνδυασμός παρακολούθησης μετατόπισης (panning) και ενεργοποίησης όταν επιτευχθεί ένα όριο. Ο χρήστης τραβάει τη λίστα προς τα κάτω, υπερνικά την αντίσταση (resistive 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 του πίνακα σε παλαιότερες εκδόσεις. Περιέχει ένα ενσωματωμένο spinner με προσαρμόσιμο χρώμα (tintColor), χαρακτηριστικό title και attributed string με ετικέτα (π.χ. „Ενημέρωση...").

Το UIRefreshControl λειτουργεί μέσω του μηχανισμού target-action: κατά την ενεργοποίηση της κίνησης, καλείται η καθορισμένη μέθοδος (π.χ. refresh(_:)). Εντός της μεθόδου εκτελείται ασύγχρονη φόρτωση δεδομένων. Μετά την ολοκλήρωση, καλείται το endRefreshing(), το οποίο κρύβει το δείκτη με κίνηση. Το UIRefreshControl διαχειρίζεται αυτόματα την ευαισθησία της κίνησης — ενεργοποιείται μόνο στην επάνω θέση του πίνακα (contentOffset.y <= 0).

Η ιδιότητα tintColor ορίζει το χρώμα του spinner. Τα χαρακτηριστικά 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 ορίζει την callback onRefresh(), στην οποία εκτελείται ασύγχρονη φόρτωση. Μετά την ολοκλήρωση, καλείται το setRefreshing(false) για απόκρυψη του δείκτη. Σημαντικό: το setRefreshing(true) καλεί ξανά το onRefresh() — επομένως για προγραμματισμένη έναρξη ενημέρωσης, χρησιμοποιήστε flag ή post μέθοδο.

Η ιδιότητα setProgressBackgroundColorSchemeResource αλλάζει το φόντο του δείκτη. setSize(SwipeRefreshLayout.LARGE) — μέγεθος spinner. Στη διάταξη XML, το SwipeRefreshLayout τυλίγει το RecyclerView: swipe_refresh_layout → recycler_view. Σύμφωνα με το Google I/O 2024, το SwipeRefreshLayout χρησιμοποιείται στο 85% των εφαρμογών Android με ροές περιεχομένου. Στην IT Sectr τυλίγουμε όλες τις οθόνες με ασύγχρονα φορτωμένες λίστες σε SwipeRefreshLayout — αυτό παρέχει ομοιόμορφη εμπειρία χρήστη σε όλες τις εκδόσεις 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 animation και προσαρμοστικό χρώμα βάσει ταπετσαρίας. Το SwipeRefreshLayout παραμένει συμβατό για εκδόσεις κάτω από Android 12.

Βέλτιστες πρακτικές και συνήθη λάθη

Pull-to-Refresh — ένα εύκολο στην υλοποίηση μοτίβο, αλλά περιέχει αρκετά συνήθη λάθη που μειώνουν την εμπειρία χρήστη. Ας τα εξετάσουμε και τους τρόπους πρόληψης.

  • Διπλή ενημέρωση — ο χρήστης μπορεί να τραβήξει τη λίστα πολλές φορές πριν ολοκληρωθεί η φόρτωση. Λύση: ορίστε το flag 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 με προσαρμοσμένο χρώμα spinner και 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 εκτελεί το coroutine στον κύκλο ζωής του fragment — κατά την καταστροφή του fragment, το coroutine ακυρώνεται αυτόματα, αποτρέποντας διαρροή μνήμης.

Παράδειγμα 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 εκτελείται ασύγχρονος κώδικας φόρτωσης δεδομένων. Το SwiftUI διαχειρίζεται αυτόματα το δείκτη ενημέρωσης και μπλοκάρει επανεκκινήσεις έως ότου ολοκληρωθεί η τρέχουσα φόρτωση — αυτή είναι η τυπική recommended προσέγγιση για νέα έργα.

Πώς να αποτρέψω τη διπλή ενημέρωση;

Χρησιμοποιήστε το flag isRefreshing: ορίστε true κατά την έναρξη φόρτωσης και false μετά την ολοκλήρωση. Στο iOS, το UIRefreshControl μπλοκάρει αυτόματα την εκ νέου κλήση έως ότου κληθεί το endRefreshing(). Στο Android, ελέγξτε το SwipeRefreshLayout.isRefreshing στην αρχή του onRefresh(): εάν true — return. Αυτό εγγυάται ένα αίτημα ανά κίνηση.

Έρχεται το Pull-to-Refresh σε σύγκρουση με την κύλιση της λίστας;

Το UIRefreshControl και το SwipeRefreshLayout ενεργοποιούνται μόνο στην επάνω θέση της λίστας (contentOffset == 0). Η αρχιτεκτονική αποκλείει τη σύγκρουση: όσο η λίστα είναι κυλιέται έστω και 1 px, η κίνηση 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 animation, προτεινόμενο για νέα έργα.
  • SwiftUI .refreshable — δηλωτικός τροποποιητής με async-closure, διαθέσιμος από iOS 15.
  • Το flag isRefreshing αποτρέπει τη διπλή ενημέρωση — υποχρεωτικό και στις δύο πλατφόρμες.
  • Το Pull-to-Refresh δεν προορίζεται για πλοήγηση — μόνο για ενημέρωση περιεχομένου σύμφωνα με Material Design και Apple HIG.

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης