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 هو نمط واجهة مستخدم يقوم فيه المستخدم بسحب (سحب لأسفل) قائمة أو منطقة قابلة للتمرير لتحديث المحتوى. بصرياً، يصحب الإجراء مؤشر تحميل (دوار) يظهر في أعلى الشاشة ويختفي بعد تلقي البيانات. تم شيوعة النمط بواسطة تطبيق 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 في خلافات الأخبار وقوائم الطلبات والدردشة حيث تكون نضارة البيانات أمرًا حاسمًا لتجربة المستخدم.

Pull-to-Refresh في iOS: UIRefreshControl

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 — مما يزيد ثقة المستخدمين في البيانات.

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 استدعاء 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 الجديد مؤشرًا متحركًا مع دعم الحركة الزنبركية ولون متكيف يعتمد على خلفية الشاشة. يبقى SwipeRefreshLayout متوافقًا للإصدارات الأقل من Android 12.

أفضل الممارسات والأخطاء الشائعة

Pull-to-Refresh هو نمط بسيط في التنفيذ، ولكنه يحتوي على عدة أخطاء شائعة تقلل من تجربة المستخدم. دعنـا نستعرضها وطرق منعها.

  • التحديث المزدوج — قد يسحب المستخدم القائمة عدة مرات قبل اكتمال التحميل. الحل: ضبط علامة 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: "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.

مثال 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+، هذا هو النهج المفضل لتنفيذ Pull-to-Refresh في SwiftUI.

الأسئلة الشائعة

هل يعمل Pull-to-Refresh في SwiftUI؟

نعم، يوفر SwiftUI المعدل .refreshable لـ List أو ScrollView، متاح منذ iOS 15. داخل closure، يتم تنفيذ كود تحميل البيانات غير المتزامن. يدير SwiftUI مؤشر التحديث تلقائيًا ويمنع الإطلاقات المتكررة حتى اكتمال التحميل الحالي — هذا هو النهج القياسي الموصى به للمشاريع الجديدة.

كيف نمنع التحديث المزدوج؟

استخدم العلامة isRefreshing: ضبطها على true عند بدء التحميل و false بعد الاكتمال. في iOS، يحظر UIRefreshControl الاستدعاءات المتكررة حتى يتم استدعاء endRefreshing(). في Android، تحقق من SwipeRefreshLayout.isRefreshing في بداية onRefresh(): إذا كان true — return. يضمن ذلك طلبًا واحدًا لكل إجراء.

هل يتعارض Pull-to-Refresh مع تمرير القائمة؟

UIRefreshControl و SwipeRefreshLayout ينشطان فقط عندما تكون القائمة في الموقع العلوي (contentOffset == 0). الهندسة تستبعد التعارض: طالما تم تمرير القائمة حتى بمقدار 1بكسل، لا ينشط إجراء Pull-to-Refresh. إذا حدث تعارض — تحقق من nestedScrollingEnabled في Android أو وجود GestureRecognizers مخصصة تعترض اللمسات.

الملخص

  • 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 جديد مع حركة زنبركية، موصى به للمشاريع الجديدة.
  • SwiftUI .refreshable — معدل تصريحي مع async closure، متاح منذ iOS 15.
  • العلامة isRefreshing تمنع التحديث المزدوج — إجبارية على كلتا المنصتين.
  • Pull-to-Refresh غير مصمم للتنقل — فقط لتحديث المحتوى وفقًا لـ Material Design و Apple HIG.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا