UIDex

السحب للتحديث

Pull to Refresh

SwipeRefreshLayout / .refreshable

اسمه كمانبول تو ريفريش، السحب لأسفل للتحديث، اسحب للتحديث، التحديث بالسحب، إيماءة التحديث، سحب لإعادة التحميل، refresh control، swipe to refresh، pull down to refresh، drag to refresh، SwipeRefreshLayout، UIRefreshControl

السحب للتحديث إيماءة تعيد جلب بيانات الشاشة المعروضة: القائمة واقفة عند أعلى نقطة فيها، فيسحبها المستخدم لأسفل، ويظهر مؤشّر من تحت الشريط العلوي، وعند تجاوز مسافة محدّدة ورفع الإصبع ينطلق الطلب. وما يميّزه عن بقية العناصر أنه بلا شكل في حالة السكون: لا زرّ ولا نصّ، فلا يعرفه إلا من جرّبه أو رآه عند غيره، ولهذا يعجز أغلب الناس عن تسميته أصلاً. وهو ليس التمرير اللانهائي (infinite scroll)، فذاك يعمل عند نهاية القائمة ويضيف الأقدم في الأسفل، أمّا هذا فيعمل عند بدايتها ويضع الأحدث في الأعلى. وليس مؤشّر تقدّم (progress indicator)، لأن الدائرة الدوّارة هنا نتيجة الإيماءة لا الإيماءة نفسها، ويمكن أن تظهر ذات الدائرة من ضغط زرّ «تحديث» بلا سحب. وليس إجراءات السحب على الصفوف (swipe actions)، فتلك إيماءة أفقية على صفّ واحد تكشف أزراراً تنتظر لمسة، وهذه إيماءة رأسية على القائمة كلها لا تكشف شيئاً يُضغط. والقاعدة العملية في المراجعة: الإيماءة تبدأ وموضع التمرير صفر، وإلا فهي شيء آخر. ولأنها غير مرئية، لا يصحّ أن تكون الطريق الوحيد لتحديث الشاشة.

لو قلت عليه…

«لمّا تشدّ الشاشة لتحت تنزل حاجات جديدة»«السحبة اللي بتخلّي الدايرة تلفّ فوق»«بسحب القايمة وأسيبها فتتحدّث لوحدها»«الحركة اللي في تطبيقات الأخبار عشان تجيب الجديد»«مفيش زرار تحديث، بتشدّ الصفحة وخلاص»

العيّنة الحيّة

تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.

الإشعارات
أفلت للتحديث
  • تم شحن طلبكقبل ٤ د
  • فاتورة يوليو جاهزةقبل ٢٠ د
  • تمّت الموافقة على الإجازةقبل ساعة
  • تقرير الأسبوعأمس
  • تحديث سياسة الاسترجاعأمس
  • رسالة من فريق الدعمالأحد
آخر تحديث ٩:٤١
RTLاسحب القائمة لأسفل. الإيماءة رأسية فلا تنعكس، والدوران يبقى مع عقارب الساعة

تشريح العنصر: كل جزء واسمه

مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.

برومبت البناء

ابنِ إيماءة سحب للتحديث فوق قائمة قابلة للتمرير. لا تُسلّح الإيماءة إلا وموضع التمرير صفر تماماً، وتجاهل السحب في أي موضع آخر حتى لا تنازع التمرير العادي. طبّق مقاومة على الإزاحة بحيث تتحرّك القائمة بنحو نصف مسافة الإصبع، واضبط عتبة إطلاق بين ٦٠ و ٨٠ dp، وضع حدّاً مطّاطياً أعلى العتبة بقليل. أظهر حالة «أفلت للتحديث» صريحة قبل رفع الإصبع: يكتمل السهم، ويتبدّل النصّ، وتصدر اهتزازة خفيفة. عند الإفلات ثبّت المؤشّر عند إزاحة قارّة، وأبقِ الصفوف القديمة ظاهرة، ولا تستبدل القائمة بهيكل تحميل. اربط توقّف الدوران بانتهاء الطلب فعلاً لا بمؤقّت، وألغِ الطلب الجاري إذا وصلت سحبة جديدة. عند النجاح أعد القائمة إلى الصفر واحفظ موضع القراءة رغم إدراج عناصر في الأعلى، ثم أعلن النتيجة نصّياً عبر منطقة aria-live="polite" أو android:accessibilityLiveRegion="polite"، وقل «لا جديد» إن لم يتغيّر شيء؛ وعند الفشل اعرض رسالة صريحة ولا تعُد بصمت. أضف دائماً طريقاً بديلاً مرئياً للتحديث (زرّ في الشريط العلوي أو عنصر في القائمة) لأن الإيماءة غير مرئية ولا يصلها مستخدم لوحة المفاتيح. الحركة رأسية فلا تضرب الإزاحة في إشارة اتجاه ولا تقلب دوران المؤشّر في RTL، لكن استعمل الخصائص المنطقية في كل ما ترسمه حول المؤشّر: padding-inline و margin-inline و inset-inline-start و text-align: start، ولا تكتب left أو right إطلاقاً، واعزل طابع الوقت بـ <bdi>. واضبط overscroll-behavior-block: contain على الحاوية حتى لا يتسلسل الفائض إلى المستند فيوقظ تحديث المتصفّح نفسه، وبدّل touch-action من pan-y إلى none أثناء السحب وحده، لأن pan-y يترك التمرير الرأسي للمتصفّح فلا تصلك أحداث المؤشّر.

سلوكه في الاتجاه من اليمين لليسار

الجزء ده حصري عندنا.

الإيماءة لا تنعكس

لا ينعكس

الإيماءة كلها على المحور الكتلي (block axis): الإصبع ينزل، والقائمة تنزل معه، واتجاه الصفحة لا يمسّ أياً من ذلك. فلا تضرب الإزاحة في إشارة اتجاه ولا تستعمل var(--dir) كما تفعل مع العناصر الأفقية، لأن قيمة clientY وحدها كافية و transform: translateY(...) صحيحة في الاتجاهين. ومن يعمّم قاعدة «كل إيماءة تنعكس» على هذا العنصر ينتهي بمؤشّر يصعد لأعلى في النسخة العربية أو بعتبة سالبة لا تتحقّق أبداً.

دوران المؤشّر

لا ينعكس

المؤشّر يلفّ مع عقارب الساعة في العربية كما في الإنجليزية، فاتجاه الدوران اصطلاح مرتبط بقراءة الساعة ولا يحمل معنى لغوياً ينقلب. والقاعدة الشاملة [dir="rtl"] svg { transform: scaleX(-1) } تقلبه إلى عكس عقارب الساعة وتقلب معه سهم السحب، فاقصر القلب على صنف تضعه بيدك على الأسهم الأفقية وحدها، ولاحظ أن transform لا مقابل منطقي له فلن يتولّى المتصفّح هذا القرار عنك. وفي UIKit يرسم UIRefreshControl دائرته بنفسه ولا يقبل صورة، فإن ركّبت مؤشّراً خاصّاً فلا تستدعِ عليه imageFlippedForRightToLeftLayoutDirection()، لأنها طلب صريح بالانعكاس والسهم الرأسي لا شيء فيه ينقلب.

نصّ المؤشّر وطابع الوقت

ينعكس

كل نصّ يرافق المؤشّر («اسحب للتحديث»، «آخر تحديث ٩:٤١») يتبع اتجاه الصفحة، فحاذِه بـ text-align: start لا left. وإن صففت الدائرة والنصّ في صفّ واحد فاستعمل gap و padding-inline بدل margin-left، لأن ترتيب الصفّ البصري ينقلب بينما ترتيب DOM يبقى كما هو. وطابع الوقت خطر خاصّ: تشغيل 12:04 PM أو اسم منطقة زمنية لاتيني داخل جملة عربية يعيد ترتيب أجزائه بصرياً بفعل خوارزمية bidi، فاعزل المقطع بـ <bdi> أو بـ unicode-bidi: isolate حتى لا تقفز PM إلى أول السطر.

السحب الأفقي على نفس القائمة

ينعكس

إن ركّبت إجراءات سحب أفقية على صفوف القائمة نفسها فتلك إيماءة على المحور المضمّن، وهي تنعكس بالكامل: ما كان يُكشف بسحب الصفّ نحو اليسار في الإنجليزية يُكشف بسحبه نحو اليمين في العربية. في SwiftUI اكتب .swipeActions(edge: .trailing)، وفي أندرويد مرّر ItemTouchHelper.START و ItemTouchHelper.END إلى SimpleCallback بدل LEFT و RIGHT، لأن الزوج الأول يُحلّ مقابل اتجاه التخطيط والثاني فيزيائي لا يعرف الاتجاه. وتبقى قيمة dX داخل onChildDraw فيزيائية على كل حال (سالبة عند التحرّك يساراً مهما كان اتجاه التخطيط)، فاقرأ الإشارة من layoutDirection قبل أن تقرّر أي زرّ ترسم خلف الصفّ.

الحشو حول المؤشّر

ينعكس

الإيماءة رأسية، لكن ما ترسمه حولها ليس كذلك، والدائرة في SwipeRefreshLayout موضوعة في منتصف العرض فلا شيء فيها ينعكس وحده. أمّا لحظة إزاحتها إلى جانب أو وضع تسمية بجوارها فاستعمل padding-inline-start و margin-inline-end على الويب و NSDirectionalEdgeInsets في UIKit. و contentInset في UIScrollView من نوع UIEdgeInsets بحقلَي left و right الفيزيائيين ولا مكافئ منطقي لهما في النوع نفسه، فما تضبطه فيهما يبقى في مكانه بعد قلب اتجاه التطبيق ويجب أن تبدّله بيدك.

أسماؤه في الكود

كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.

SwiftUI.refreshable { await reload() }ضعه على List فيملك النظام الإيماءة كلها؛ الدوران يتوقّف حين تعود الدالة غير المتزامنة.
UIKitscrollView.refreshControl = UIRefreshControl()الواجهة الأقدم، وفيها endRefreshing() مسؤوليتك أنت؛ نسيانها يترك الدائرة تلفّ إلى الأبد.
AndroidXSwipeRefreshLayout.setOnRefreshListenerيلفّ ابناً واحداً قابلاً للتمرير فقط، و setRefreshing(false) يدوي بالكامل.
Jetpack ComposePullToRefreshBox(isRefreshing, onRefresh)واجهة Material 3 الحالية، وتحتها Modifier.pullToRefresh و PullToRefreshState لبناء مؤشّر خاصّ.
CSSoverscroll-behavior-block: containيمنع تسلسل الفائض إلى المستند، وهو التسلسل الذي يوقظ تحديث المتصفّح نفسه.
ARIAaria-live="polite" + aria-busy="true"لا وجود لإيماءة أصيلة على الويب، فالإعلان النصّي هو كل ما يصل قارئ الشاشة.
DOMelement.setPointerCapture(pointerId)يبقي أحداث المؤشّر واصلة إلى الحاوية بعد خروج الإصبع من حدودها أثناء السحب.

شوف كمان

آخر تحديث · 2026-08-19