{"id":"l-rn-2-3","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"اجعل شاشات Trailhead تناسب أي هاتف باستخدام useWindowDimensions وهوامش الـ safe area، واكتب كودًا خاصًا بكل منصّة باستخدام Platform.select أو ملفات منفصلة لكل منصّة، فقط حيث تختلف المنصّتان فعلًا.","html":"<p>تتراوح الهواتف التي يحملها مستخدموك بين أجهزة Android صغيرة بعرض نحو 360 نقطة وأجهزة iPhone كبيرة يتجاوز عرضها 430، إضافةً إلى الأجهزة اللوحية، والهواتف القابلة للطي التي يتغيّر عرضها في منتصف الجلسة، والوضع الأفقي. وفوق ذلك، الشاشة ليست مستطيلًا نظيفًا: هناك نتوء (notch) أو Dynamic Island، وزوايا مستديرة، ومؤشّر الشاشة الرئيسية، وعلى Android أشرطة الحالة والتنقّل التي يرسم تطبيقك الآن خلفها. ثلاث أدوات تجعل Trailhead يناسب كل ذلك.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">قراءة حجم الشاشة بالطريقة الصحيحة</h2>\n<p>يُرجع <code dir=\"ltr\">useWindowDimensions()</code> القيم <code dir=\"ltr\">width</code> و<code dir=\"ltr\">height</code> و<code dir=\"ltr\">scale</code> و<code dir=\"ltr\">fontScale</code> للنافذة، ويعيد الـ render للـ component كلما تغيّرت:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useWindowDimensions } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> PhotoGrid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">photos</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">photos</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] }) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useWindowDimensions</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> columns</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> width </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 700</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 4</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …render photos in `columns` columns</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>ستجد كودًا قديمًا يستدعي <code dir=\"ltr\">Dimensions.get('window')</code> على مستوى الوحدة (module). هذا يقرأ الحجم مرة واحدة، حين يُحمَّل الملف، ولا يقرؤه بعدها أبدًا: أدِر الهاتف، أو افتح التطبيق في وضع تقسيم الشاشة على iPad، أو افتح هاتفًا قابلًا للطي، وسيبقى التخطيط يستخدم العرض القديم. استخدم الـ hook داخل الـ components.</p>\n<p>وفضّل الـ flex على القياسات حين تستطيع. معظم التخطيطات لا تحتاج إلى العرض أصلًا؛ استخدم <code dir=\"ltr\">useWindowDimensions</code> حين تتغيّر <strong>البنية</strong> مع الحجم (عمودان يصبحان أربعة)، لا لحساب قيم بالبكسل يتولّاها الـ flexbox أصلًا.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>احترم fontScale</span></p>\n<p>تكون <code dir=\"ltr\">fontScale</code> أكبر من 1 حين يكبّر المستخدم حجم نص النظام؛ وقيمة 1.3 أو أكثر شائعة بين المتنزّهين الأكبر سنًّا، وهم جمهور Trailhead. اختبر شاشاتك بأكبر حجم نص لإمكانية الوصول مرة واحدة على الأقل. الصفوف ذات الارتفاعات الثابتة أول ما ينكسر.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ safe areas على المنصّتين</h2>\n<p>الـ <strong>safe area</strong> (المنطقة الآمنة) هي جزء الشاشة الذي لا تغطّيه فتحات العتاد ولا واجهة النظام. ترويسات التنقّل وأشرطة الـ tabs في Expo Router تحترمها أصلًا. أما الشاشات التي لا تحويها، مثل خريطة بملء الشاشة أو صفحة تعريفية، فيجب أن تتعامل معها بنفسها.</p>\n<p>المكتبة المخصّصة لذلك هي <code dir=\"ltr\">react-native-safe-area-context</code>. يتضمّنها Expo Router أصلًا ويوفّر الـ <code dir=\"ltr\">SafeAreaProvider</code> في الجذر، فيمكنك استخدام الـ hook مباشرة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/app/track.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { StyleSheet, Text, View } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useSafeAreaInsets } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native-safe-area-context'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> TrackScreen</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> insets</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useSafeAreaInsets</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">styles.screen</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* the map fills the whole screen, edge to edge */</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[styles.topBar, { top: insets.top </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 8</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">styles.timer</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>01:24:10&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[styles.bottomBar, { paddingBottom: insets.bottom </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Stop and save&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> styles</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StyleSheet.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">create</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  screen: { flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  topBar: { position: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'absolute'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, left: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, right: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  timer: { fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontWeight: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'700'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  bottomBar: { position: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'absolute'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, left: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, right: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, bottom: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, paddingTop: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يمكن للخريطة أن تُرسم تحت شريط الحالة، وهذا يبدو جميلًا، بينما تستقرّ عناصر التحكّم تحت المنطقة غير الآمنة بالضبط. للكائن <code dir=\"ltr\">insets</code> القيم <code dir=\"ltr\">top</code> و<code dir=\"ltr\">bottom</code> و<code dir=\"ltr\">left</code> و<code dir=\"ltr\">right</code>، فيعمل الوضع الأفقي أيضًا. وتصدّر المكتبة كذلك component باسم <code dir=\"ltr\">SafeAreaView</code> يطبّق الحشو نيابةً عنك، وهو مفيد للشاشات البسيطة.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استخدام SafeAreaView من react-native</span></p>\n<p>ما زال <code dir=\"ltr\">import { SafeAreaView } from 'react-native'</code> يُترجَم، لكنه مُهمَل، ولم يعمل يومًا إلا على iOS، ويتجاهل تخطيط Android من الحافة إلى الحافة. في إصدارات Android الحديثة ترسم التطبيقات خلف شريطَي الحالة والتنقّل، فينتهي المحتوى الذي لا يستخدم الهوامش تحت الساعة أو تحت شريط الإيماءات. استورده من <code dir=\"ltr\">react-native-safe-area-context</code> بدلًا من ذلك.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">حين تختلف المنصّتان</h2>\n<p>يشارك React Native الكود بين iOS وAndroid، لكن المنصّتين ليستا متطابقتين، والتظاهر بأنهما كذلك ينتج تطبيقًا يبدو غريبًا على كلتيهما. يعطيك <code dir=\"ltr\">Platform</code> نظام التشغيل الحالي:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Platform, StyleSheet } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> styles</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StyleSheet.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">create</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  title: {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">17</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    fontWeight: Platform.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">OS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'ios'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '600'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '500'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  card: Platform.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ios: { boxShadow: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0 2px 8px rgba(0, 0, 0, 0.12)'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    android: { elevation: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    default: {},</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>يُرجع <code dir=\"ltr\">Platform.select</code> القيمة الخاصة بالمنصّة الحالية، ويعود إلى <code dir=\"ltr\">default</code> إن لم يجدها. إنه الأداة المناسبة للفروق الصغيرة: وزن خط، أو ظلّ، أو hit slop.</p>\n<p>وحين يختلف <strong>component بأكمله</strong>، قسّمه إلى ملفات. أنشئ <code dir=\"ltr\">DatePicker.ios.tsx</code> و<code dir=\"ltr\">DatePicker.android.tsx</code>، ثم استورد <code dir=\"ltr\">./DatePicker</code> دون امتداد؛ فيختار Metro الملف المناسب لكل منصّة. والأمر نفسه يعمل مع <code dir=\"ltr\">.native.tsx</code> و<code dir=\"ltr\">.web.tsx</code> إن عمل Trailhead يومًا على الويب.</p>\n<p>أين تختلف المنصّتان فعلًا؟ في أعراف التنقّل (iOS يرجع بالسحب من الحافة اليسرى؛ وAndroid لديه إيماءة رجوع على مستوى النظام)، وفي أدوات الاختيار وحقول التاريخ، وفي الخطوط الافتراضية، والاهتزاز اللمسي (haptics)، ومسارات طلب الأذونات. وحيث لا تختلفان، كقائمة الرحلات والنماذج ومنطق العمل، شارك كل شيء.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">شبكة تناسب الأجهزة اللوحية</h2>\n<p>لنجمع القطع معًا: يجب أن تعرض شبكة الصور أكبر عدد من الأعمدة يتّسع، مع حدّ أدنى لعرض البطاقة، ومسافات متّسقة، وحشو للشاشة. هذا حساب صغير يُنفَّذ في كل مرة يتغيّر فيها العرض، وهو بالضبط نوع الدوال النقية الذي يستحق أن تكتبه بشكل صحيح مرة واحدة. ستكتبه في التمرين أدناه.</p>\n<p>التالي: القوائم. سيحمل Trailhead عمّا قليل مئات الرحلات، ورسمها كلها دفعة واحدة أسرع طريقة لجعل الهاتف يبدو بطيئًا.</p>\n","toc":[{"id":"sec-1","text":"قراءة حجم الشاشة بالطريقة الصحيحة","level":2},{"id":"sec-2","text":"الـ safe areas على المنصّتين","level":2},{"id":"sec-3","text":"حين تختلف المنصّتان","level":2},{"id":"sec-4","text":"شبكة تناسب الأجهزة اللوحية","level":2}],"takeaways":["<code dir=\"ltr\">useWindowDimensions()</code> يعيد الـ render عند التدوير وتغيير الحجم؛ أما <code dir=\"ltr\">Dimensions.get()</code> فلقطة واحدة تصبح قديمة.","الشاشات التي لا تحوي ترويسة تنقّل أو شريط tabs يجب أن تضيف حشوها بنفسها باستخدام هوامش الـ safe area من react-native-safe-area-context.","يرسم Android التطبيقات من الحافة إلى الحافة خلف أشرطة النظام، لذا تهمّ الهوامش على Android بقدر ما تهمّ على iOS.","استخدم <code dir=\"ltr\">Platform.select</code> للفروق الصغيرة، وملفّي <code dir=\"ltr\">.ios.tsx</code> / <code dir=\"ltr\">.android.tsx</code> حين يختلف component بأكمله."],"further":[{"title":"Safe areas","url":"https://docs.expo.dev/develop/user-interface/safe-areas/"},{"title":"useWindowDimensions","url":"https://reactnative.dev/docs/usewindowdimensions"},{"title":"Platform-Specific Code","url":"https://reactnative.dev/docs/platform-specific-code"}],"quiz":[{"q":"<p>تحسب شبكة الصور في Trailhead عدد أعمدتها مرة واحدة باستخدام <code dir=\"ltr\">Dimensions.get('window').width</code> في أعلى الملف. ما الذي ينكسر؟</p>","options":[{"text":"لا شيء؛ عرض الشاشة لا يتغيّر أبدًا أثناء عمل التطبيق.","why":"يتغيّر عند التدوير، وفي وضع تقسيم الشاشة على iPad، وعلى الهواتف القابلة للطي، والقيمة المخزّنة لا تتحدّث."},{"text":"تحتفظ الشبكة بعدد أعمدتها القديم بعد أن يدير المستخدم الهاتف أو يفتح هاتفًا قابلًا للطي.","why":"صحيح. اقرأ الحجم باستخدام <code dir=\"ltr\">useWindowDimensions()</code> داخل الـ component ليُعاد الـ render بالعرض الجديد."},{"text":"ينهار على Android لأن <code dir=\"ltr\">Dimensions</code> خاص بـ iOS.","why":"<code dir=\"ltr\">Dimensions</code> يعمل على المنصّتين. المشكلة أن القيمة تُقرأ مرة واحدة ولا تُحدَّث أبدًا."}],"answer":1},{"q":"<p>خريطة Trailhead بملء الشاشة ليس لها ترويسة، وأزرارها العلوية تقع تحت الـ Dynamic Island في iPhone. ما الإصلاح الصحيح؟</p>","options":[{"text":"أضف <code dir=\"ltr\">paddingTop: 50</code> إلى الشاشة.","why":"المقدار الصحيح يختلف بحسب الجهاز والاتجاه والمنصّة. والرقم السحري خاطئ على معظم الهواتف."},{"text":"غلّف الشاشة بـ <code dir=\"ltr\">SafeAreaView</code> المستورد من <code dir=\"ltr\">react-native</code>.","why":"هذا الـ component مُهمَل (deprecated)، ويعمل على iOS فقط، ولا يتعامل مع تخطيط Android من الحافة إلى الحافة."},{"text":"أخفِ شريط الحالة فلا يتداخل شيء.","why":"الـ Dynamic Island والزوايا المستديرة من العتاد؛ وإخفاء شريط الحالة لا يحرّكها."},{"text":"اقرأ <code dir=\"ltr\">useSafeAreaInsets()</code> وطبّق <code dir=\"ltr\">insets.top</code> على حاوية الأزرار.","why":"صحيح. الهوامش هي الحجم الدقيق للمنطقة غير الآمنة على هذا الجهاز، في هذه اللحظة، وعلى المنصّتين."}],"answer":3},{"q":"<p>يحتاج منتقي التاريخ في Trailhead إلى components مختلفة كليًا على iOS وAndroid، بينما بقية النموذج مشتركة. ما البنية الأنظف؟</p>","options":[{"text":"ملفان، <code dir=\"ltr\">DatePicker.ios.tsx</code> و<code dir=\"ltr\">DatePicker.android.tsx</code>، يُستوردان باسم <code dir=\"ltr\">./DatePicker</code>.","why":"صحيح. يختار المُجمِّع الملف المناسب لكل منصّة، ويبقى كل ملف بسيطًا وسهل القراءة."},{"text":"component واحد مع <code dir=\"ltr\">if (Platform.OS === 'ios')</code> حول كل سطر مختلف.","why":"هذا يصلح لسطر أو اثنين، لكن component كاملًا مليئًا بالتفرّعات صعب القراءة وسهل الكسر."},{"text":"تطبيقان منفصلان، واحد لكل منصّة.","why":"هذا يرمي منطق النموذج المشترك، وهو السبب الرئيسي لاستخدام React Native."}],"answer":0}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Work out how many photo cards fit across the screen, and how wide each one is.\nfunction gridLayout(screenWidth, { minCardWidth = 160, gap = 12, padding = 16 } = {}) {\n  const columns = Math.floor(screenWidth / minCardWidth);\n  const cardWidth = screenWidth / columns;\n  return { columns, cardWidth };\n}\n\nconsole.log(gridLayout(390)); // iPhone in portrait\nconsole.log(gridLayout(820)); // iPad in portrait\n"},"solution":{"js":"function gridLayout(screenWidth, { minCardWidth = 160, gap = 12, padding = 16 } = {}) {\n  const available = screenWidth - padding * 2;\n  const columns = Math.max(1, Math.floor((available + gap) / (minCardWidth + gap)));\n  const cardWidth = Math.floor((available - gap * (columns - 1)) / columns);\n  return { columns, cardWidth };\n}\n\nconsole.log(gridLayout(390)); // iPhone in portrait\nconsole.log(gridLayout(820)); // iPad in portrait\n"},"checks":[{"id":"phone","test":"return typeof gridLayout === 'function' && (() => { const r = gridLayout(390); return r && r.columns === 2 && r.cardWidth === 173; })();"},{"id":"small-phone","test":"return (() => { const r = gridLayout(320); return r && r.columns === 1 && r.cardWidth === 288; })();"},{"id":"tablet","test":"return (() => { const r = gridLayout(820); return r && r.columns === 4 && r.cardWidth === 188; })();"},{"id":"options","test":"return (() => { const r = gridLayout(400, { minCardWidth: 100, gap: 8, padding: 20 }); return r && r.columns === 3 && r.cardWidth === 114; })();"},{"id":"never-zero","test":"return (() => { const r = gridLayout(200, { minCardWidth: 300 }); return r && r.columns === 1 && r.cardWidth === 168; })();"}],"title":"اجعل شبكة الصور تناسب أي شاشة","prompt":"<p>تستدعي شبكة الصور في Trailhead الدالة <code dir=\"ltr\">gridLayout(width)</code> بالقيمة القادمة من <code dir=\"ltr\">useWindowDimensions()</code>. أصلح <code dir=\"ltr\">gridLayout(screenWidth, options)</code> لتُرجع <code dir=\"ltr\">{ columns, cardWidth }</code> بحيث:</p>\n<ul>\n<li>للشاشة حشو بمقدار <code dir=\"ltr\">padding</code> على اليسار <strong>و</strong>اليمين، وبين البطاقات مسافة <code dir=\"ltr\">gap</code> نقطة (دون مسافة عند الحافتين الخارجيتين)،</li>\n<li><code dir=\"ltr\">columns</code> هو <strong>أكبر</strong> عدد من البطاقات يكون عرض كلٍّ منها <code dir=\"ltr\">minCardWidth</code> على الأقل، ولا يقلّ أبدًا عن 1،</li>\n<li><code dir=\"ltr\">cardWidth</code> هو العرض الذي تحصل عليه كل بطاقة حين تُقسَم المساحة المتبقية بالتساوي، مقرَّبًا <strong>إلى الأسفل</strong> إلى نقطة كاملة.</li>\n</ul>\n<p>القيم الافتراضية: <code dir=\"ltr\">minCardWidth = 160</code>، <code dir=\"ltr\">gap = 12</code>، <code dir=\"ltr\">padding = 16</code>. مثلًا، iPhone بعرض 390 نقطة يعطي <code dir=\"ltr\">{ columns: 2, cardWidth: 173 }</code>.</p>\n","hints":["ابدأ بالمساحة التي يمكنك استخدامها فعلًا: <code dir=\"ltr\">screenWidth - padding * 2</code>.","n بطاقة تحتاج إلى n عرضًا وn - 1 مسافة. لذا تتّسع n بطاقة حين يتحقّق <code dir=\"ltr\">n * minCardWidth + (n - 1) * gap &lt;= available</code>، وبإعادة الترتيب: <code dir=\"ltr\">n &lt;= (available + gap) / (minCardWidth + gap)</code>.","ثم <code dir=\"ltr\">cardWidth = Math.floor((available - gap * (columns - 1)) / columns)</code>، وغلّف عدد الأعمدة بـ <code dir=\"ltr\">Math.max(1, …)</code>."],"explanation":"<p>تجاهل الكود المبدئي الحشو والمسافات، ففاضت البطاقات خارج الشاشة، وكان يمكن أن يُرجع صفر أعمدة على شاشة ضيّقة (والقسمة على صفر تعطي <code dir=\"ltr\">Infinity</code>). يبدأ الإصلاح من العرض القابل للاستخدام، ويعدّ كم وحدة من «بطاقة زائد مسافة» تتّسع (مع مسافة أقل بواحدة من عدد البطاقات)، ويضمن عمودًا واحدًا على الأقل، ويقسم العرض المتبقي بالتساوي. ولأنها دالة نقية تعتمد على العرض وحده، تتحدّث الشبكة بشكل صحيح عند التدوير وعلى الهواتف القابلة للطي بمجرد أن يُبلغ <code dir=\"ltr\">useWindowDimensions</code> عن حجم جديد.</p>\n","checkLabels":{"phone":"هاتف بعرض 390 نقطة يحصل على عمودين بعرض 173","small-phone":"هاتف بعرض 320 نقطة يحصل على عمود واحد بعرض 288","tablet":"جهاز لوحي بعرض 820 نقطة يحصل على 4 أعمدة بعرض 188","options":"تُحترَم القيم المخصّصة لـ minCardWidth وgap وpadding","never-zero":"الشاشة الأضيق من بطاقة واحدة تحصل مع ذلك على عمود واحد"}},"words":594,"readMinutes":3}