النقاط الرئيسية
Pull-to-Refresh هو نمط واجهة مستخدم يقوم فيه المستخدم بسحب (سحب لأسفل) قائمة أو منطقة قابلة للتمرير لتحديث المحتوى. بصرياً، يصحب الإجراء مؤشر تحميل (دوار) يظهر في أعلى الشاشة ويختفي بعد تلقي البيانات. تم شيوعة النمط بواسطة تطبيق Tweetie لـ iPhone (2008) ثم توحيده لاحقًا من Apple (iOS 6 — UIRefreshControl) و Google (Android Support Library — SwipeRefreshLayout).
من وجهة نظر فنية، Pull-to-Refresh هو مزيج من التحريك (تتبع إزاحة الإصبع) ومحفز عند بلوغ الحد. يسحب المستخدم القائمة لأسفل، متغلبًا على المقاومة (التمرير المقاوم)، وبعد تجاوز الحد (~80بكسل في iOS، ~64ديبي في Android)، يبدأ تحريك المؤشر والتحميل غير المتزامن. إذا أفلت الإصبع قبل الحد — تعود القائمة إلى وضعها الأصلي دون تحديث.
وفقًا لإرشادات Material Design، يجب عدم استخدام Pull-to-Refresh للتنقل أو تبديل العلامات التبويبية — فالغرض الوحيد له هو تحديث البيانات. في IT Sectr، نستخدم Pull-to-Refresh في خلافات الأخبار وقوائم الطلبات والدردشة حيث تكون نضارة البيانات أمرًا حاسمًا لتجربة المستخدم.
UIRefreshControl هو عنصر تحكم قياسي في iOS لتقنية Pull-to-Refresh، متاح منذ iOS 6. يتم إضافة UIRefreshControl إلى UITableViewController عبر الخاصية refreshControl (iOS 10+) أو كعرض فرعي للجدول في الإصدارات السابقة. يحتوي على مؤشر دوار مضمن بلون قابل للتخصيص (tintColor)، وسمة title، وسلسلة منسوبة بتعليق (على سبيل المثال، «جارٍ التحديث...»).
يعمل UIRefreshControl من خلال آلية target-action: عند تفعيل الإجراء، يتم استدعاء الطريقة المحددة (على سبيل المثال، refresh(_:)). داخل الطريقة، يتم تنفيذ تحميل غير متزامن للبيانات. بعد الاكتمال، يتم استدعاء endRefreshing()، والذي يخفي المؤشر بحركة. يدير UIRefreshControl حساسية الإجراء تلقائيًا — يتم تفعيله فقط عندما تكون القائمة في الموقع العلوي (contentOffset.y <= 0).
تحدد الخاصية tintColor لون المؤشر. تسمح سمات title بعرض نص مثل «تم التحديث منذ دقيقتين» بعد الاكتمال. منذ 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 استدعاء 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.
بدءًا من Android 12 (Material You)، توصي Google باستخدام Material Pull-to-Refresh الجديد من مكتبة material-1.6.0+ (androidx.compose.material3.pulltorefresh لـ Compose). يستخدم API الجديد مؤشرًا متحركًا مع دعم الحركة الزنبركية ولون متكيف يعتمد على خلفية الشاشة. يبقى SwipeRefreshLayout متوافقًا للإصدارات الأقل من Android 12.
Pull-to-Refresh هو نمط بسيط في التنفيذ، ولكنه يحتوي على عدة أخطاء شائعة تقلل من تجربة المستخدم. دعنـا نستعرضها وطرق منعها.
في IT Sectr، أضفنا فحص isRefreshing في كل مشروع بعد اكتشاف طلبات مكررة في سجلات خادم الاختبار — اتضح أن المستخدمين ذوي الأصابع السريعة كانوا يشغلون التحديث حتى 3 مرات متتالية.
يضيف Pull-to-Refresh إلى UITableViewController بلون مؤشر مخصص و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: "Pull down to refresh"
)
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 أداة مشتركة داخل دورة حياة القطعة — عند إتلاف القطعة، تلغى الأداة تلقائيًا، مما يمنع تسرب الذاكرة.
يوفر 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+، هذا هو النهج المفضل لتنفيذ Pull-to-Refresh في SwiftUI.
الأسئلة الشائعة
نعم، يوفر SwiftUI المعدل .refreshable لـ List أو ScrollView، متاح منذ iOS 15. داخل closure، يتم تنفيذ كود تحميل البيانات غير المتزامن. يدير SwiftUI مؤشر التحديث تلقائيًا ويمنع الإطلاقات المتكررة حتى اكتمال التحميل الحالي — هذا هو النهج القياسي الموصى به للمشاريع الجديدة.
استخدم العلامة isRefreshing: ضبطها على true عند بدء التحميل و false بعد الاكتمال. في iOS، يحظر UIRefreshControl الاستدعاءات المتكررة حتى يتم استدعاء endRefreshing(). في Android، تحقق من SwipeRefreshLayout.isRefreshing في بداية onRefresh(): إذا كان true — return. يضمن ذلك طلبًا واحدًا لكل إجراء.
UIRefreshControl و SwipeRefreshLayout ينشطان فقط عندما تكون القائمة في الموقع العلوي (contentOffset == 0). الهندسة تستبعد التعارض: طالما تم تمرير القائمة حتى بمقدار 1بكسل، لا ينشط إجراء Pull-to-Refresh. إذا حدث تعارض — تحقق من nestedScrollingEnabled في Android أو وجود GestureRecognizers مخصصة تعترض اللمسات.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.