دليلك الشامل: بناء تطبيق كاميرا احترافي باستخدام Expo و React Native
يُعد Expo بيئة عمل متكاملة ومجموعة أدوات قوية تُسهّل بشكل كبير عملية بناء تطبيقات React Native. فهو يقلل من تعقيد إعداد بيئة التطوير ويوفر عليك عناء تثبيت وتكوين العديد من المكونات الأساسية. في هذا الدليل الشامل، سنتعلم خطوة بخطوة كيفية إنشاء تطبيق كاميرا بسيط لكنه فعال، يتيح للمستخدمين التقاط الصور، معاينتها، استخدام وضع الفلاش، والتبديل بسلاسة بين الكاميرتين الأمامية والخلفية.
المتطلبات الأساسية لبدء تطوير تطبيق الكاميرا
لا يتطلب Expo الكثير لبدء بناء أول تطبيق React Native لك. يمكنك استكشاف المزيد حول تثبيت Expo و expo-cli في الوثائق الرسمية.
ملاحظة: في هذا الشرح، سنستخدم نظامي التشغيل macOS و iOS. ومع ذلك، يمكنك استخدام Android أيضًا، حيث لا يوجد فرق جوهري في استخدام Expo في هذه المرحلة.
يمكنك تثبيت Expo و expo-cli بشكل عام (globally) عن طريق تشغيل الأمر التالي في سطر الأوامر:
npm install --global expo-cli
يتطلب Expo وجود بيئة Node.js ليعمل بشكل صحيح. يمكنك الحصول على أحدث إصدار من Node.js من الموقع الرسمي.
البدء في العمل: تهيئة مشروع Expo جديد
بعد تثبيت Expo و Node.js بنجاح، يمكنك البدء في إنشاء مشروع Expo جديد باستخدام الأمر التالي:
expo init expo-camera-app
تثبيت الحزم وتشغيل التطبيق على جهازك
يوفر لنا Expo تطبيقًا عميلاً (client app) يمكننا من خلاله تشغيل التطبيق الذي نبنيه ومعاينة نتائجه مباشرة. هذا التطبيق متاح للتنزيل على كل من App Store و Google Play.
هذه هي واجهة التطبيق التي ستراها:
تهيئة مشروع Expo: اختيار القالب المناسب
انتقل إلى دليل التطبيق الذي تم إنشاؤه حديثًا وقم بتشغيل الأمر التالي:
cd expo-camera-app
سيُطلب منك الإجابة على بعض الأسئلة لاختيار القالب الافتراضي للتطبيق. في هذا الدليل، سنختار خيار blank (TypeScript)، ولكن يمكنك دائمًا اختيار ما يناسب احتياجات مشروعك.

تشغيل التطبيق ومعاينته على الأجهزة
بعد تهيئة المشروع، يمكننا تشغيل التطبيق باستخدام الأمر expo run:

سيؤدي هذا إلى فتح نافذة في متصفحك حيث يمكنك رؤية سجلات (logs) التطبيق. كما سيقوم بتوليد رمز QR يمكنك مسحه ضوئيًا لتشغيل التطبيق على جهازك الفعلي. الميزة الرائعة في Expo هي أنك لست بحاجة إلى تثبيت وتكوين المحاكيات (simulators) لتشغيل التطبيق، على الرغم من أنه يمنحك خيار تشغيل Expo على المحاكي إذا قمت بتثبيته وتكوينه بنفسك.
بالعودة إلى تطبيقنا، بافتراض أنك قمت بتشغيل التطبيق بنجاح على جهازك، ستكون هذه هي الشاشة الافتراضية:
افتح دليل التطبيق في محرر الأكواد المفضل لديك (مثل VS Code). سيبدو ملف App.tsx على النحو التالي:
import {StatusBar} from 'expo-status-bar'
import React from 'react'
import {StyleSheet, Text, View} from 'react-native'
export default function App ( ) {
return (
< View style = {styles.container} >
< Text > Open up App.tsx to start working on your app! </ Text >
< StatusBar style = "auto" />
</ View >
)
}
const styles = StyleSheet.create({
container : {
flex : 1 ,
backgroundColor : '#fff' ,
alignItems : 'center' ,
justifyContent : 'center'
}
})
بناء واجهة المستخدم (UI) لتطبيق الكاميرا
بعد تشغيل المشروع بنجاح، حان الوقت الآن للبدء في بناء واجهة المستخدم.
تثبيت حزمة الكاميرا Expo
الخطوة التالية هي تثبيت حزمة expo-camera، وذلك باستخدام الأمر التالي:
expo install expo-camera
سنقوم بإنشاء واجهة مستخدم بسيطة تسمح للمستخدم ببدء عملية استخدام الكاميرا.
import {StatusBar} from 'expo-status-bar'
import React from 'react'
import {StyleSheet, Text, View, TouchableOpacity} from 'react-native'
export default function App ( ) {
return (
< View style = {{ flex: 1 , backgroundColor: '# fff ', justifyContent: ' center ', alignItems: ' center ' }} >
< TouchableOpacity style = {{ width: 130 , borderRadius: 4 , backgroundColor: '# 14274e ', flexDirection: ' row ', justifyContent: ' center ', alignItems: ' center ', height: 40 }} >
< Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
</ TouchableOpacity >
</ View >
)
}
const styles = StyleSheet.create({
container : {
flex : 1 ,
backgroundColor : '#fff' ,
alignItems : 'center' ,
justifyContent : 'center'
}
})
هذه واجهة مستخدم بسيطة: قمنا باستيراد TouchableOpacity للزر وأضفنا بعض التنسيقات الأساسية.
الآن، نحتاج إلى استخدام hook من نوع useState لإدارة الحالة وعرض معاينة الكاميرا عندما يضغط المستخدم على زر “التقاط الصورة”.
<TouchableOpacity onPress={__startCamera} style={{ width : 130 , borderRadius : 4 , backgroundColor : '#14274e' , flexDirection : 'row' , justifyContent : 'center' , alignItems : 'center' , height : 40 }} >
< Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
</TouchableOpacity>
const [startCamera,setStartCamera] = React.useState( false )
const __startCamera = ()=> {
// Function logic will go here
}
هناك أمران مهمان يجب القيام بهما عندما يضغط المستخدم على الزر:
- طلب الإذن للوصول إلى الكاميرا. في تطوير تطبيقات الجوال، غالبًا ما يكون الوصول إلى العديد من واجهات برمجة التطبيقات (
APIs) والميزات الأصلية (native features) مقيدًا بأذونات المستخدم والخصوصية. هذا أمر يجب أن تعتاد عليه عند تطوير تطبيقات الجوال. - تغيير الحالة وعرض الكاميرا.
لنقم باستيراد وحدة الكاميرا من expo-camera باستخدام هذا الأمر:
import {Camera} from 'expo-camera'
ثم أضف عرض الكاميرا (camera view) على النحو التالي:
<Camera style={{ flex : 1 , width : "100%" }} ref={ ( r ) => { camera = r }} ></Camera>
يمكننا استخدام السمة ref للوصول إلى وظائف الكاميرا (camera's methods):
let camera: Camera
عند الضغط على زر “التقاط الصورة” (take picture button)، سيتم استدعاء الدالة __startCamera:
const __startCamera = async () => {
const {status} = await Camera.requestPermissionsAsync()
if (status === 'granted' ){
// Do something if permission is granted
} else {
Alert.alert( "Access denied" )
}
}
ستطلب الدالة الإذن أولاً. إذا منح المستخدم الوصول إلى الكاميرا، يمكننا المتابعة وفتح الكاميرا. وإلا، سنعرض تنبيهًا بسيطًا.
إضافة مكون الكاميرا وعرضها شرطيًا
لنقم بعرض الكاميرا عندما يمنح المستخدم الوصول إلى كاميرا الجهاز:
const __startCamera = async () => {
const {status} = await Camera.requestPermissionsAsync()
if (status === 'granted' ) {
// Start the camera
setStartCamera( true )
} else {
Alert.alert( 'Access denied' )
}
}
يجب علينا إجراء بعض التغييرات على واجهة المستخدم (UI) وإضافة عرض شرطي (conditional rendering). سنعرض الكاميرا فقط عندما يطلبها المستخدم، وإلا سنعرض الشاشة الافتراضية.
{startCamera ? (
< Camera style = {{flex: 1 , width: " 100 %"}} ref = {(r) => { camera = r }} >
</ Camera >
) : (
< View style = {{ flex: 1 , backgroundColor: '# fff ', justifyContent: ' center ', alignItems: ' center ' }} >
< TouchableOpacity onPress = {__startCamera} style = {{ width: 130 , borderRadius: 4 , backgroundColor: '# 14274e ', flexDirection: ' row ', justifyContent: ' center ', alignItems: ' center ', height: 40 }} >
< Text style = {{ color: '# fff ', fontWeight: ' bold ', textAlign: ' center ' }} > Take picture </ Text >
</ TouchableOpacity >
</ View >
)}

رائع، الآن نحتاج إلى إضافة زر حتى نتمكن من التقاط الصورة الفعلية.
إضافة زر التقاط الصورة
هذا عبارة عن View بسيط داخل عرض الكاميرا (camera view) له موضع مطلق (absolute position). لذا نتأكد من أنه دائمًا في الجزء العلوي من الكاميرا.
<View style={{ position : 'absolute' , bottom : 0 , flexDirection : 'row' , flex : 1 , width : '100%' , padding : 20 , justifyContent : 'space-between' }} >
< View style = {{ alignSelf: ' center ', flex: 1 , alignItems: ' center ' }} >
< TouchableOpacity onPress = {__takePicture} style = {{ width: 70 , height: 70 , bottom: 0 , borderRadius: 50 , backgroundColor: '# fff ' }} />
</ View >
</View>
آلية التقاط الصورة وعرض المعاينة
يجب أن يلتقط التطبيق صورة عند الضغط على زر الالتقاط. ستبدو هذه الدالة على النحو التالي:
const __takePicture = async () => {
if (!camera) return
const photo = await camera.takePictureAsync()
}
أولاً، نتحقق من أن لدينا وصولاً إلى مكون Camera باستخدام ref:
if (!camera) return // إذا كانت الكاميرا غير معرفة أو فارغة (null)، نوقف تنفيذ الدالة
ثم نلتقط الصورة عن طريق استدعاء الدالة takePictureAsync. تُرجع هذه الدالة وعدًا (promise) وكائنًا (object) يحتوي على تفاصيل الصورة. ستبدو النتيجة هكذا:
Object {
"height" : 4224 ,
"uri" : "file:///var/mobile/Containers/Data/Application/E6740A15-93AF-4120-BF11-6E8B74AFBF93/Library/Caches/ExponentExperienceData/%2540anonymous%252Fcamera-app-ee0fa3c8-1bb1-4d62-9863-33bf26341c55/Camera/19F0C5DD-7CA6-4043-8D89-AF65A1055C7E.jpg" ,
"width" : 1952 ,
}
نحن مهتمون فقط بمسار الصورة (Picture URL) الذي يُشار إليه بـ uri. بعد التقاط الصورة، يجب أن نعرض معاينة الصورة ونخفي عرض الكاميرا. للقيام بذلك، سنستخدم اثنين من الـ hooks لتغيير الحالة:
const [previewVisible, setPreviewVisible] = useState( false )
const [capturedImage, setCapturedImage] = useState<any>( null )
const __takePicture = async () => {
if (!camera) return
const photo = await camera.takePictureAsync()
console .log(photo)
setPreviewVisible( true )
setCapturedImage(photo)
}
نستخدم setPreviewVisible(true) لعرض المعاينة و setCapturedImage(photo) لتخزين كائن النتيجة.
ثم نعرض المعاينة على النحو التالي:
{previewVisible && capturedImage ? (
< CameraPreview photo = {capturedImage} />
) : (
< Camera style = {{flex: 1 }} ref = {(r) => { camera = r }} >
< View style = {{ flex: 1 , width: ' 100 %', backgroundColor: ' transparent ', flexDirection: ' row ' }} >
< View style = {{ position: ' absolute ', bottom: 0 , flexDirection: ' row ', flex: 1 , width: ' 100 %', padding: 20 , justifyContent: ' space-between ' }} >
< View style = {{ alignSelf: ' center ', flex: 1 , alignItems: ' center ' }} >
< TouchableOpacity onPress = {__takePicture} style = {{ width: 70 , height: 70 , bottom: 0 , borderRadius: 50 , backgroundColor: '# fff ' }} />
</ View >
</ View >
</ View >
</ Camera >
)}
مكون CameraPreview يبدو كالتالي:
const CameraPreview = ( {photo}: any ) => {
console .log( 'sdsfds' , photo)
return (
< View style = {{ backgroundColor: ' transparent ', flex: 1 , width: ' 100 %', height: ' 100 %' }} >
< ImageBackground source = {{uri: photo && photo.uri }} style = {{ flex: 1 }} />
</ View >
)
}
والنتيجة تبدو هكذا:

إعادة التقاط الصورة: خيارات إضافية للمعاينة
يمكننا إضافة بعض الأزرار إلى شاشة المعاينة التي ستسمح للمستخدم بأداء المزيد من الإجراءات، مثل إعادة التقاط الصورة أو حفظها.
أضف خاصيتي savePhoto و retakePicture إلى مكون CameraPreview على النحو التالي:
<CameraPreview photo={capturedImage} savePhoto={__savePhoto} retakePicture={__retakePicture} />
عند الضغط على زر “إعادة التقاط” (Re-take)، سيتعين علينا إخفاء المعاينة، وإزالة الصورة الحالية، وعرض الكاميرا مرة أخرى. نفعل ذلك باستخدام الكود التالي:
const __retakePicture = () => {
setCapturedImage( null )
setPreviewVisible( false )
__startCamera()
}

إضافة خيارات متقدمة: الكاميرا الأمامية/الخلفية والفلاش
توفر حزمة expo-camera العديد من الخيارات لتخصيص الكاميرا، مثل وضع الفلاش (FlashMode)، وتحديد نوع الكاميرا (أمامية/خلفية)، والتكبير (zooming)، وغير ذلك.
تفعيل وضع الفلاش (FlashMode)
لنضف خيارًا يتيح للمستخدم تشغيل وضع الفلاش أو إيقافه:

نقوم ببساطة بإنشاء زر صغير لتبديل الفلاش بين التشغيل والإيقاف، هكذا:
<TouchableOpacity onPress={__handleFlashMode} style={{ position : 'absolute' , left : '5%' , top : '10%' , backgroundColor : flashMode === 'off' ? '#000' : '#fff' , borderRadius : '50%' , height : 25 , width : 25 }} >
< Text style = {{ fontSize: 20 }} > ⚡️ </ Text >
</TouchableOpacity>
ونقوم بتغيير الحالة (state) عند الضغط على الزر:
const [flashMode, setFlashMode] = React.useState( 'off' )
const __handleFlashMode = () => {
if (flashMode === 'on' ) {
setFlashMode( 'off' )
} else if (flashMode === 'off' ) {
setFlashMode( 'on' )
} else {
setFlashMode( 'auto' )
}
}
ثم نضيف خاصية flashMode إلى مكون الكاميرا:
<Camera flashMode={flashMode} style={{ flex : 1 }} ref={ ( r ) => { camera = r }} ></Camera>
التبديل بين الكاميرا الأمامية والخلفية
سنضيف زرًا للتبديل بين الكاميرا الخلفية والأمامية. يمكننا الحصول على نوع الكاميرا الافتراضي مباشرة من وحدة الكاميرا كما يلي:
const [cameraType, setCameraType] = React.useState(Camera.Constants.Type.back)
أضف خاصية type على النحو التالي:
<Camera type={cameraType} flashMode={flashMode} style={{ flex : 1 }} ref={ ( r ) => { camera = r }} ></Camera>
ثم أضف زر التبديل:
<TouchableOpacity onPress={__switchCamera} style={{ marginTop : 20 , borderRadius : '50%' , height : 25 , width : 25 }} >
< Text style = {{ fontSize: 20 }} > {cameraType === 'front' ? '?' : '?'} </ Text >
</TouchableOpacity>
ودالة التبديل:
const __switchCamera = () => {
if (cameraType === 'back' ) {
setCameraType( 'front' )
} else {
setCameraType( 'back' )
}
}
هذه هي النتيجة:

يمكنك العثور على الكود المصدري الكامل لهذا المشروع على GitHub.
الخلاصة التقنية
بشكل عام، يُعد Expo أداة مذهلة يمكنها توفير الكثير من الوقت والجهد للمطورين. فهو يتيح لك البدء في بناء التطبيقات مباشرة دون الحاجة إلى القلق بشأن إعداد بيئة التطوير المعقدة. ومع ذلك، في بعض الحالات المتقدمة، قد تحتاج إلى بناء ملحقات أصلية (native extensions) أو التعامل مع ميزات الجهاز بطرق مخصصة. في هذه السيناريوهات، قد يكون استخدام React Native CLI خيارًا أفضل، حيث يمنحك تحكمًا كاملاً في الكود الأصلي (native code) ويسمح لك بتعديله بسهولة لتلبية متطلباتك الخاصة. يظل Expo خيارًا ممتازًا للمشاريع التي تتطلب سرعة التطوير وتبسيط العمليات، بينما يوفر React Native CLI المرونة القصوى للمطورين ذوي الاحتياجات المحددة على مستوى النظام الأصلي.