الوحدة 1 · النموذج الذهني
الـ components الأساسية: View وText وImage وScrollView
20 دقراءة 4 داكتشف الخطأ
الدرس في لمحة
ابنِ شاشة تفاصيل الرحلة في Trailhead باستخدام View وText وImage وScrollView، وتجنّب قواعد النص والصور التي تُسقط شاشات الجوّال أو تتركها فارغة.
- 1View: صندوق، لا أكثر
- 2Text: المكان الوحيد للنصوص
- 3Image: أخبره بحجمه
- 4ScrollView: محتوى قد لا يتّسع
- 5بقية المجموعة الأساسية
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
على الويب، يمكنك أن ترمي نصًّا داخل div، وتضبط الخط على body، وتضع <img> دون أن تفكّر في حجمها. جرّب الشيء نفسه في React Native وستحصل على شاشة خطأ حمراء، أو نص بخط خاطئ، أو صورة لا تظهر أبدًا. أربعة components تحمل معظم الشاشات، ولكلٍّ منها قاعدة واحدة تُوقع مطوّري الويب.
View: صندوق، لا أكثر
View هو حجر البناء في التخطيط: مستطيل يمكن أن يكون له خلفية وحدود وحشو (padding) وأبناء. ويقابله view حاوية أصلي. ليس فيه عقدة نصية، لذا لا يمكن أن يحوي نصوصًا:
// Throws: Text strings must be rendered within a <Text> component.
<View>Ridge Loop</View>
// Works
<View>
<Text>Ridge Loop</Text>
</View>فكّر في View على أنه الـ div الذي تستخدمه للتخطيط، مطروحًا منه كل ما يفعله الـ div مع النص. وهو أيضًا أكثر ما ستنسّقه: البطاقات والصفوف والفواصل والشارات والمسافات الفارغة كلها Views لها خلفية أو حواف مستديرة أو بعض الحشو. الـ Views رخيصة لكنها ليست مجانية؛ فكل واحد منها يصبح native view حقيقيًا، وصفّ في قائمة فيه اثنتا عشرة طبقة تغليف متداخلة يكلّف أكثر من صفّ فيه أربع. قلّل الطبقات حين لا تضيف الطبقة شيئًا.
Text: المكان الوحيد للنصوص
كل نص مرئي يوضع داخل <Text>. عناصر <Text> المتداخلة تصبح فقرة واحدة، وهذا أيضًا المكان الوحيد الذي توجد فيه وراثة التنسيق: الـ Text الابن يرث الخط واللون والحجم من Text الأب.
<Text style={{ fontSize: 16, color: '#333' }}>
Ridge Loop is <Text style={{ fontWeight: '700' }}>8.4 km</Text> with 420 m of climbing.
</Text>الـ View لا يمرّر تنسيقات النص إلى أسفل أبدًا. وإن أردت خطًّا موحّدًا في التطبيق كله، فاصنع component (مثل AppText يضبط القيم الافتراضية التي تريدها) بدلًا من البحث عن ملف تنسيق عام غير موجود.
والنص أيضًا يتكيّف مع إعدادات إمكانية الوصول (accessibility) لدى المستخدم. من ضبط حجم خط أكبر في إعدادات iOS أو Android يحصل على نص أكبر في تطبيقك تلقائيًا، وهذا جيد، لكنه يعني أن أي صندوق بارتفاع ثابت حول النص سيقصّه في النهاية. اترك مجالًا للنمو، وفضّل الحشو على الارتفاعات الثابتة في أي شيء يحوي كلمات.
وستستخدم اثنين من الـ props باستمرار: numberOfLines={2} يقتطع النص ويضيف علامة حذف (مثالي لبطاقات القوائم)، وselectable يسمح للمستخدمين بنسخ النص.
Image: أخبره بحجمه
للصور المضمّنة في التطبيق، يحلّ require() الملف وقت البناء، فيعرف React Native أبعاد الصورة:
<Image source={require('../../assets/images/trail-placeholder.png')} />أما صور الشبكة، فلا يستطيع React Native معرفة حجمها حتى ينتهي التنزيل، لذا يجب أن تعطيه حجمًا. دون ذلك تُرسم الصورة بحجم 0 في 0، بصمت.
<Image
source={{ uri: hike.photoUrl }}
style={{ width: '100%', height: 220, borderRadius: 12 }}
resizeMode="cover"
accessibilityLabel={`Photo from ${hike.name}`}
/>تعمل resizeMode مثل object-fit في CSS: cover تملأ وتقصّ، وcontain تحتوي الصورة كاملة. وفي تطبيقات الإنتاج تستخدم فرق كثيرة expo-image بدلًا منه: له الشكل الأساسي نفسه، ويضيف التخزين المؤقت على القرص والصور المؤقتة (placeholders)، ويستخدم contentFit بدلًا من resizeMode.
ScrollView: محتوى قد لا يتّسع
الـ native views لا تتمرّر عند تجاوز المحتوى لحدودها. التمرير component مستقل اسمه ScrollView، وله خاصّيتان غريبتان.
أولًا، يحتاج إلى ارتفاع محدود: فهو يتمرّر داخل المساحة التي يمنحه إياها الأب، لذا تحتاج سلسلة الآباء إلى flex: 1 (سترى السبب في درس الـ flexbox). ثانيًا، الحشو والمحاذاة للمحتوى المتمرّر توضع في contentContainerStyle، لا في style. فـ style يحدّد حجم نافذة التمرير، وcontentContainerStyle ينسّق المحتوى الذي ينزلق داخلها.
هذه شاشة التفاصيل في Trailhead، مع بيانات تجريبية حاليًا:
import { Image, ScrollView, Text, View } from 'react-native';
const hike = {
name: 'Ridge Loop',
photoUrl: 'https://images.example.com/ridge-loop.jpg',
distanceKm: 8.4,
elevationM: 420,
notes: 'Steep first kilometre, then a long ridge with views of the lake. Bring water: the spring at the saddle was dry.',
photoCount: 0,
};
export default function HikeScreen() {
return (
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 12 }}>
<Image
source={{ uri: hike.photoUrl }}
style={{ width: '100%', height: 220, borderRadius: 12 }}
accessibilityLabel={`Photo from ${hike.name}`}
/>
<Text style={{ fontSize: 28, fontWeight: '700' }}>{hike.name}</Text>
<View style={{ flexDirection: 'row', gap: 16 }}>
<Text>{hike.distanceKm} km</Text>
<Text>{hike.elevationM} m up</Text>
</View>
{hike.photoCount > 0 && <Text>{hike.photoCount} photos</Text>}
<Text style={{ fontSize: 16, lineHeight: 24 }}>{hike.notes}</Text>
</ScrollView>
);
}لاحظ ما ليس موجودًا: لا div، ولا أسماء classes، ولا استيراد لملف تنسيق، ولا نصوص شاردة. والأرقام مثل hike.distanceKm موجودة داخل Text، فتُرسم دون مشكلة.
بقية المجموعة الأساسية
ستتعرّف على البقية حين يحتاج إليها Trailhead: Pressable وTextInput في الدرس التالي، وFlatList وSectionList للقوائم، وActivityIndicator للتحميل، وSwitch لمفاتيح التبديل، وModal للطبقات العائمة، وKeyboardAvoidingView للنماذج. كلٌّ منها يقابله component أصلي، فتبدو وتتصرّف كما تتصرّف عناصر المنصّة افتراضيًا.
في الدرس التالي ستجعل Trailhead تفاعليًا: أزرار تستجيب للمس، وحقول إدخال تتعامل مع الكتابة.
أهم النقاط
Viewصندوق للتخطيط ولا يمكن أن يحوي نصًّا خامًا؛ كل نصّ يجب أن يكون داخل<Text>.- تنسيقات النص تُورَث فقط من
<Text>أب، ولا تُورَث أبدًا من<View>. - صور الشبكة تحتاج إلى عرض وارتفاع صريحين؛ أما الصور المحلية المحمّلة عبر
require()فتعرف حجمها بنفسها. ScrollViewيرسم كل أبنائه دفعة واحدة ويحتاج إلى ارتفاع محدود، فاستخدمه لشاشات المحتوى، لا لقوائم البيانات الطويلة.{count && <Text>…</Text>}ينهار حين تكونcountصفرًا، لأن 0 يُرسم كنصّ خام؛ استخدم مقارنة صريحة بدلًا من ذلك.