Κύρια σημεία
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 σε ροές ειδήσεων, λίστες παραγγελιών και συνομιλίες, όπου η φρεσκάδα των δεδομένων είναι κρίσιμη για την εμπειρία χρήστη.
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 — αυτό αυξάνει την εμπιστοσύνη των χρηστών στα δεδομένα.
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.
Από το 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 — ένα εύκολο στην υλοποίηση μοτίβο, αλλά περιέχει αρκετά συνήθη λάθη που μειώνουν την εμπειρία χρήστη. Ας τα εξετάσουμε και τους τρόπους πρόληψης.
Στην IT Sectr προσθέσαμε τον έλεγχο isRefreshing σε κάθε έργο αφού ανακαλύψαμε διπλότυπα αιτήματα στα αρχεία καταγραφής του διακομιστή δοκιμών — αποδείχθηκε ότι χρήστες με γρήγορα δάχτυλα ενεργοποιούσαν την ενημέρωση έως και 3 φορές στη σειρά.
Προσθέτει Pull-to-Refresh στο UITableViewController με προσαρμοσμένο χρώμα spinner και attributed title. Μετά τη φόρτωση δεδομένων, ο δείκτης κρύβεται.
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.
Τυλίγει το RecyclerView σε SwipeRefreshLayout με προσαρμοσμένα χρώματα δείκτη. Το onRefresh ξεκινά τη φόρτωση και κρύβει το δείκτη μετά την ολοκλήρωση.
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 ακυρώνεται αυτόματα, αποτρέποντας διαρροή μνήμης.
Το σύγχρονο SwiftUI παρέχει τον τροποποιητή .refreshable, ο οποίος προσθέτει αυτόματα Pull-to-Refresh σε List ή ScrollView.
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.
Συχνές ερωτήσεις
Ναι, το SwiftUI παρέχει τον τροποποιητή .refreshable για List ή ScrollView, διαθέσιμο από το iOS 15. Εντός του closure εκτελείται ασύγχρονος κώδικας φόρτωσης δεδομένων. Το SwiftUI διαχειρίζεται αυτόματα το δείκτη ενημέρωσης και μπλοκάρει επανεκκινήσεις έως ότου ολοκληρωθεί η τρέχουσα φόρτωση — αυτή είναι η τυπική recommended προσέγγιση για νέα έργα.
Χρησιμοποιήστε το flag isRefreshing: ορίστε true κατά την έναρξη φόρτωσης και false μετά την ολοκλήρωση. Στο iOS, το UIRefreshControl μπλοκάρει αυτόματα την εκ νέου κλήση έως ότου κληθεί το endRefreshing(). Στο Android, ελέγξτε το SwipeRefreshLayout.isRefreshing στην αρχή του onRefresh(): εάν true — return. Αυτό εγγυάται ένα αίτημα ανά κίνηση.
Το UIRefreshControl και το SwipeRefreshLayout ενεργοποιούνται μόνο στην επάνω θέση της λίστας (contentOffset == 0). Η αρχιτεκτονική αποκλείει τη σύγκρουση: όσο η λίστα είναι κυλιέται έστω και 1 px, η κίνηση Pull-to-Refresh δεν ενεργοποιείται. Εάν προκύψει σύγκρουση — ελέγξτε το nestedScrollingEnabled στο Android ή την ύπαρξη προσαρμοσμένων GestureRecognizer που υποκλέπτουν τα αγγίγματα.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης