الفرق بين المراجعتين ل"ReactNative/native components ios"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
سطر 90: سطر 90:
 
import PropTypes from 'prop-types';
 
import PropTypes from 'prop-types';
 
import React from 'react';
 
import React from 'react';
import {requireNativeComponent} from 'react-native';
+
import { requireNativeComponent } from 'react-native';
  
 
class MapView extends React.Component {
 
class MapView extends React.Component {
سطر 102: سطر 102:
 
   * A Boolean value that determines whether the user may use pinch
 
   * A Boolean value that determines whether the user may use pinch
 
   * gestures to zoom in and out of the map.
 
   * gestures to zoom in and out of the map.
  * قيمة منطقية للسّماح للمستخدم بالقَرْصِ للتكبير والتصغير أو منعه من ذلك
 
 
   */
 
   */
   zoomEnabled: PropTypes.bool,
+
   zoomEnabled: PropTypes.bool
 
};
 
};
  
سطر 111: سطر 110:
 
module.exports = MapView;
 
module.exports = MapView;
 
</syntaxhighlight>
 
</syntaxhighlight>
لدينا الآن مكوّن مُغلَّف وموثَّق سهلُ الاستخدام. لاحظ أننا غيَّرنا معامل الدالة ‎‎<code>requireNativeComponent</code>‎‎ الثاني من ‎‎<code>null</code>‎‎ إلى المكوّن <code>MapView</code> المُغلِّف الجديد. يسمح هذا للبنية التحتيّة بالتحقق من أنّ أنواع <code>propTypes</code> تتطابق مع الخاصيات الأصيلة لتقليل فرص عدم التطابق بين شيفرة Objective-C وشيفرة JavaScript.
+
لدينا الآن مكوّن مُغلَّف وموثَّق سهلُ الاستخدام. لاحظ أننا غيَّرنا معامل الدالة ‎‎<code>requireNativeComponent</code>‎‎ الثاني من ‎‎<code>null</code>‎‎ إلى المكوّن <code>MapView</code> المُغلِّف الجديد. يسمح هذا للبنية التحتيّة بالتحقق من أنّ أنواع propTypes تتطابق مع الخاصيات الأصيلة لتقليل فرص عدم التطابق بين شيفرة Objective-C وشيفرة JavaScript.
  
 
بعد ذلك، لِنُضِف الخاصيّة ‎‎<code>region</code>‎‎ الأعقَد. نبدأ بإضافة الشيفرة الأصيلة:
 
بعد ذلك، لِنُضِف الخاصيّة ‎‎<code>region</code>‎‎ الأعقَد. نبدأ بإضافة الشيفرة الأصيلة:
سطر 127: سطر 126:
 
// RNTMapManager.m
 
// RNTMapManager.m
  
#import "RCTConvert+Mapkit.m"
+
#import "RCTConvert+Mapkit.h"
  
 
// RCTConvert+Mapkit.h
 
// RCTConvert+Mapkit.h
سطر 183: سطر 182:
 
   * The region is defined by the center coordinates and the span of
 
   * The region is defined by the center coordinates and the span of
 
   * coordinates to display.
 
   * coordinates to display.
 
+
   *المنطقة التي ستُعرَض في الخريطة
   * المنطقة التي ستُعرَض في الخريطة
 
 
   *
 
   *
 
   * تُحدَّد المنطقة عبر إحداثيات المركز ومجالها
 
   * تُحدَّد المنطقة عبر إحداثيات المركز ومجالها
 
   */
 
   */
 
   region: PropTypes.shape({
 
   region: PropTypes.shape({
     /**
+
     /**  
    * إحداثيات مركز الخريطة
+
      * إحداثيات مركز الخريطة
 +
    * Coordinates for the center of the map.
 
     */
 
     */
 
     latitude: PropTypes.number.isRequired,
 
     latitude: PropTypes.number.isRequired,
سطر 197: سطر 196:
 
     /**
 
     /**
 
     * Distance between the minimum and the maximum latitude/longitude
 
     * Distance between the minimum and the maximum latitude/longitude
 +
    * to be displayed.
 
     * المسافة بين الحد الأدنى والأقصى لكل من خطي العرض والطول
 
     * المسافة بين الحد الأدنى والأقصى لكل من خطي العرض والطول
 
     */
 
     */
سطر 227: سطر 227:
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
 
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
   nativeOnly: {onChange: true},
+
   nativeOnly: { onChange: true }
 
});
 
});
 
</syntaxhighlight>
 
</syntaxhighlight>
سطر 265: سطر 265:
  
 
#import "RNTMapView.h"
 
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.m"
+
#import "RCTConvert+Mapkit.h"
  
 
@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
 
@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
سطر 312: سطر 312:
 
في التابع المفوَّض ‎‎<code>-mapView:regionDidChangeAnimated:</code>‎‎ تُستدعى كتلة معالج الأحداث على العرض المتوافق مع بيانات المنطقة. ينتج عن استدعاء كتلة معالج الأحداث <code>onRegionChange</code> استدعاءُ نفس خاصيّة رد النداء في JavaScript. تُستدعى دالّة رد النداء هذه مع الحدث الأولي، والذي نقوم بمعالجته عادةً في المكوِّن المُغلِّف لتبسيط واجهة برمجة التطبيقات:
 
في التابع المفوَّض ‎‎<code>-mapView:regionDidChangeAnimated:</code>‎‎ تُستدعى كتلة معالج الأحداث على العرض المتوافق مع بيانات المنطقة. ينتج عن استدعاء كتلة معالج الأحداث <code>onRegionChange</code> استدعاءُ نفس خاصيّة رد النداء في JavaScript. تُستدعى دالّة رد النداء هذه مع الحدث الأولي، والذي نقوم بمعالجته عادةً في المكوِّن المُغلِّف لتبسيط واجهة برمجة التطبيقات:
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 +
 
// MapView.js
 
// MapView.js
  
سطر 334: سطر 335:
 
MapView.propTypes = {
 
MapView.propTypes = {
 
   /**
 
   /**
   * دالة رد نداء تُستدعى باستمرار عندما يسحب المستخدم الخريطة
+
   * Callback that is called continuously when the user is dragging the map.
 +
* دالة رد نداء تُستدعى باستمرار عندما يسحب المستخدم الخريطة
 
   */
 
   */
 
   onRegionChange: PropTypes.func,
 
   onRegionChange: PropTypes.func,
سطر 344: سطر 346:
 
class MyApp extends React.Component {
 
class MyApp extends React.Component {
 
   onRegionChange(event) {
 
   onRegionChange(event) {
     // تعامل مع القيمة
+
     // Do stuff with event.region.latitude, etc.
 +
  // تعامل مع القيمة
 
     // event.region.latitude
 
     // event.region.latitude
 
     // وغيرها من القيم
 
     // وغيرها من القيم
سطر 366: سطر 369:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
== التعامل مع عدة عروض أصيلة ==
 +
يمكن أن يكون لعرض React Native عدة أبناء في شجرة العروض، مثلاً:<syntaxhighlight lang="javascript">
 +
<View>
 +
  <MyNativeView />
 +
  <MyNativeView />
 +
  <Button />
 +
</View>
 +
</syntaxhighlight>في هذا المثال يغلف الصنف <code>MyNativeView</code> المكون <code>NativeComponent</code> ويقدم الدوال التي ستستدعى على منصة iOS. يعرَّف <code>MyNativeView</code> في  <code>MyNativeView.ios.js</code> ويحوي دوال الوكيل الموجودة في <code>NativeComponent.</code>
 +
 +
عندما يتفاعل المستخدم مع المكون بالنقر على الزر يتغير backgroundColor الخاص بــ MyNativeView، في هذه الحالة لن يعلم UIManager أي MyNativeView يجب أن يعالج وأي منها سيغير backgroundColor، ستحل هذه المشكلة بالطريقة التالية:<syntaxhighlight lang="javascript">
 +
<View>
 +
  <MyNativeView ref={this.myNativeReference} />
 +
  <MyNativeView ref={this.myNativeReference2} />
 +
  <Button
 +
    onPress={() => {
 +
      this.myNativeReference.callNativeMethod();
 +
    }}
 +
  />
 +
</View>
 +
</syntaxhighlight>الأى يحوي المكون مرجعًا إلى <code>MyNativeView</code> محدّد مما يسمح لنا باستخدام نسخة محددة من <code>MyNativeView</code>. الآن يستطيع الزر أن يعرف لأي <code>MyNativeView</code> يجب أن يغير <code>backgroundColor</code> .في هذا المثال لنفرض أن <code>callNativeMethod</code> هي التي تغير <code>backgroundColor</code>
 +
 +
سيحوي <code>MyNativeView.ios.js</code> شيفرة كما يلي:<syntaxhighlight lang="javascript">
 +
class MyNativeView extends React.Component {
 +
  callNativeMethod = () => {
 +
    UIManager.dispatchViewManagerCommand(
 +
      ReactNative.findNodeHandle(this),
 +
      UIManager.getViewManagerConfig('RNCMyNativeView').Commands
 +
        .callNativeMethod,
 +
      []
 +
    );
 +
  };
 +
 +
  render() {
 +
    return <NativeComponent ref={NATIVE_COMPONENT_REF} />;
 +
  }
 +
}
 +
</syntaxhighlight><code>callNativeMethod</code> هي دالتنا المخصصة في iOS والتي تغير <code>backgroundColor</code> كمثال، والتي تعرض من خلال <code>MyNativeView</code>. تستخدم هذه الدالة UIManager.dispatchViewManagerCommand والتي تحتاج ثلاثة معاملات:
 +
 +
* <code>(nonnull NSNumber \*)reactTag</code> : الرقم المعرف لعرض react.
 +
* <code>commandID:(NSInteger)commandID</code> : الرقم المعرف للدالة الأصيلةالتي يجب استدعاؤها.
 +
* <code>commandArgs:(NSArray<id> \*)commandArgs</code> : معاملات الدالة الأصيلة التي يمكن أن نمررها من JS  إلى native.
 +
 +
<code>RNCMyNativeViewManager.m</code><syntaxhighlight lang="javascript">
 +
#import <React/RCTViewManager.h>
 +
#import <React/RCTUIManager.h>
 +
#import <React/RCTLog.h>
 +
 +
RCT_EXPORT_METHOD(callNativeMethod:(nonnull NSNumber*) reactTag) {
 +
    [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
 +
        NativeView *view = viewRegistry[reactTag];
 +
        if (!view || ![view isKindOfClass:[NativeView class]]) {
 +
            RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
 +
            return;
 +
        }
 +
        [view callNativeMethod];
 +
    }];
 +
 +
}
 +
</syntaxhighlight>تعرَّف <code>callNativeMethod</code> في الملف <code>RNCMyNativeViewManager.m،</code> وتحوي معاملًا واحدًا فقط هو <code>(nonnull NSNumber*) reactTag</code>. ستجد هذه الدالة المصدرة علاضًا معينًا يستخدم <code>addUIBlock</code> ويحوي المعامل <code>viewRegistry</code> ويعيد المكون اعتمادًا على <code>reactTag</code> مما يسمح له باستدعاء الدالة على المكون الصحيح.
 +
 
==الأنماط (Styles)==
 
==الأنماط (Styles)==
 
لأنّ جميع عروض React الأصيلة الخاصة بنا هي أصناف فرعية من الصنف <code>UIView</code>، فإنّ معظم سمات الأنماط (style attributes) ستعمل كالمتوقّع تلقائيًّا. لكن ستحتاج بعض المكونات إلى نمط افتراضي، مثلًا، <code>UIDatePicker</code> يحتاج إلى حجم ثابت. هذا النمط الافتراضي مهم لتعمل خوارزمية التخطيط كما يجب، لكننا نريد أيضًا التمكّن من الكتابة فوق النمط الافتراضي وتغييره عند استخدام المكوّن. يقوم المكوّن <code>DatePickerIOS</code> بهذا عن طريق تغليف المكون الأصيل في عرض إضافي يتميّز بتنسيق مرن، وباستخدام نمط ثابت (الذي يولَّد بثوابت تُمرَّر من الجزء الأصيل) على المكون الأصيل الداخلي:
 
لأنّ جميع عروض React الأصيلة الخاصة بنا هي أصناف فرعية من الصنف <code>UIView</code>، فإنّ معظم سمات الأنماط (style attributes) ستعمل كالمتوقّع تلقائيًّا. لكن ستحتاج بعض المكونات إلى نمط افتراضي، مثلًا، <code>UIDatePicker</code> يحتاج إلى حجم ثابت. هذا النمط الافتراضي مهم لتعمل خوارزمية التخطيط كما يجب، لكننا نريد أيضًا التمكّن من الكتابة فوق النمط الافتراضي وتغييره عند استخدام المكوّن. يقوم المكوّن <code>DatePickerIOS</code> بهذا عن طريق تغليف المكون الأصيل في عرض إضافي يتميّز بتنسيق مرن، وباستخدام نمط ثابت (الذي يولَّد بثوابت تُمرَّر من الجزء الأصيل) على المكون الأصيل الداخلي:
سطر 393: سطر 457:
 
   },
 
   },
 
});
 
});
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
 
تُصدَّر ثوابت <code>RCTDatePickerIOSConsts</code> من الجزء الأصيل عبر الحصول على الإطار (frame) الفعلي للمكون الأصيل كما يلي:
 
تُصدَّر ثوابت <code>RCTDatePickerIOSConsts</code> من الجزء الأصيل عبر الحصول على الإطار (frame) الفعلي للمكون الأصيل كما يلي:
سطر 414: سطر 479:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
غطَّى هذا الدليل العديد من جوانب تجسير المكونات الأصيلة المخصصة، ولكن هناك الكثير مما قد تحتاج إلى أخذه على عين الاعتبار، مثل الخطافات المخصصة (custom hooks) لإدراج العروض الفرعية وتخطيطها. إذا أردت التعمق أكثر، انظر الشيفرة المصدريّة لبعض من [https://github.com/facebook/react-native/blob/master/React/Views مكوّنات React Native].
+
غطَّى هذا الدليل العديد من جوانب تجسير المكونات الأصيلة المخصصة، ولكن هناك الكثير مما قد تحتاج إلى أخذه على عين الاعتبار، مثل الخطافات المخصصة (custom hooks) لإدراج العروض الفرعية وتخطيطها. إذا أردت التعمق أكثر، انظر [https://github.com/facebook/react-native/blob/master/React/Views الشيفرة المصدريّة] لبعض من مكوّنات React Native.
  
 
== مصادر ==
 
== مصادر ==
 
* [https://facebook.github.io/react-native/docs/native-components-ios صفحة Native UI Components في توثيق React Native الرسمي.]
 
* [https://facebook.github.io/react-native/docs/native-components-ios صفحة Native UI Components في توثيق React Native الرسمي.]
 
[[تصنيف:ReactNative]]
 
[[تصنيف:ReactNative]]

مراجعة 13:57، 26 مارس 2021


هناك الكثير من أدوات واجهة المستخدم (UI widgets) الأصيلة الجاهزة للاستخدام في أحدث التطبيقات، بعضها جزء من المنصّة، والبعض الآخر متاح كمكتبات تابعة لجهات طرف ثالث (third-party libraries)، إضافة إلى ما تستخدمه أنت كذلك. يحتوي React Native على العديد من مكوّنات المنصّة الأكثر أهمية، وهي جاهزة للاستخدام، مثل ScrollView وTextInput، ولكن ليس جميعها، وبالطبع، فهذا لا يشمل تلك التي قد تكون كتبتها بنفسك. لحسن الحظ، من السهل جدًا تغليف هذه المكونات الحالية لإدماجها بسلاسة مع تطبيق React Native الخاص بك.

على غرار صفحة الوحدات الأصيلة، فهذا الدليل كذلك أكثر تقدمًا، ويَفترِض أنك معتاد إلى حد ما على برمجة iOS. سيوضح لك هذا الدليل كيفية إنشاء مكون واجهة مستخدم أصيل، وسيرشدك إلى كتابة مجموعة فرعيّة من مكون MapView الموجود في مكتبة React Native الأساسية (the core React Native library).

مثال MapView لنظام iOS

لنفترض أننا نريد إضافة خريطة تفاعلية إلى تطبيقنا، لنستخدم مكتبة MKMapView، سنحتاج فقط إلى جعلها قابلة للاستخدام من جهة JavaScript.

تُنشأ العروض الأصيلة وتُعالَج (أو تُدارُ) بواسطة أصناف فرعية من الصنف RCTViewManager. تشبه هذه الصفات الفرعية في وظيفتها مُتحكّماتِ العروض (view controllers)، لكنّها أساسًا مفردة (singletons)، أي أن الجسر يُنشئ نسخة واحدة فقط من كل منها. وتُوفّر الوصول إلى العروض الأصيلة للصنف RCTUIManager، الذي يُفوِّضها مرة أخرى لتعيين وتحديث خاصيات العروض حسب الضرورة. عادةً ما تكون أصناف RCTViewManager هي المفوَّضَةُ لتمثيل العروض (delegates for the views)، مُرسِلةً الأحداثَ إلى JavaScript مُجدّدًا عبر الجسر.

عمليّة توفير الوصول إلى عرضٍ بسيطة:

  • أنشئ صنفًا فرعيًّا من RCTViewManager لإنشاء مُعالجٍ لإدارة مُكوِّنك.
  • أضف ماكرو التعليم (marker macro) لتصدير الوحدة بالسطر ‎‎RCT_EXPORT_MODULE()‎‎.
  • اكتب التابع ‎‎-(UIView *)view‎‎.
// RNTMapManager.m
#import <MapKit/MapKit.h>

#import <React/RCTViewManager.h>

@interface RNTMapManager : RCTViewManager
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

- (UIView *)view
{
  return [[MKMapView alloc] init];
}

@end

ملاحظة: لا تحاول تعيين الخاصيّة ‎‎frame‎‎ أو ‎‎backgroundColor‎‎على نسخة UIView التي وفَّرْتَ الوصول إليها من خلال التابع ‎‎-view‎‎. سيكتب React Native فوق القيم التي تُعيِّنها بواسطة صنفك المُخصَّص للتوافق مع خاصيّات JavaScript المسؤولة عن تخطيط (layout) مكوِّنِك. إذا كنت بحاجة إلى دقة التحكم هذه، فقد يكون من الأفضل تغليف نسخةِ الصنف UIView الذي تريد تصميمه في صنف UIView آخر وإعادة الصّنف UIView المُغلَّف بدلاً من ذلك. انظر هذه الصفحة للاستزادة.

في المثال أعلاه، قمنا باستعمال البادئة RNT في اسم الصنف الذي أنشأناه. تُستخدَم البادئات لتجنب تضارب الأسماء (name collisions) مع الأطر الأخرى. تستخدم أطر عمل Apple بادئات مؤلفة من حرفين، ويستخدم React Native المقطع RCT كبادئة. نوصي باستخدام بادئة من ثلاثة أحرف بخلاف RCT في أصنافك الخاصّة لتفادي تضارب الأسماء.

بعد ذلك، ستحتاج فقط إلى القليل من شيفرة JavaScript لجعله مكونَ React جاهز للاستخدام:

// MapView.js

import { requireNativeComponent } from 'react-native';

// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap');

// MyApp.js

import MapView from './MapView.js';

...

render() {
  return <MapView style={{ flex: 1 }} />;
}

تأكد من استخدام RNTMap هنا. نريد طَلَب (أو استيراد: require) المُعالج هنا، والذي سيُوفِّر الوصول إلى عرض المُعالج لاستخدامه في JavaScript.

ملاحظة: عند التصيير (rendering)، لا تنس تمديد العرض كي لا تظهر شاشةٌ فارغة.

  render() {
    return <MapView style={{flex: 1}} />;
  }

أصبح هذا المكون الآن عرضَ خريطة أصيلٍ كاملِ الوظائف قابلٍ للاستخدام في JavaScript، مع دعم كامل المميّزات كالقَرْصِ للتكبير وغيرها من الإيماءات الأصيلة. لكن للأسف لا يمكننا التحكم به من JavaScript حتى الآن.

الخاصيّات

أول ما يمكننا القيام به لجعل هذا المكوّن قابلًا للاستخدام أكثر هو نقل بعض الخاصيّات الأصيلة عبر الجسر. لنقل أننا نريد التمكّن من تعطيل التكبير وتحديد المنطقة المرئية. يُمكن تعطيل التكبير بقيمة منطقيّة بسيطة، لذا سنضيف هذا السطر:

// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

لاحظ أننا نحدّد النّوع بوضوح على أنّه قيمة منطقيّة (‎‎BOOL‎‎) لأنّ React Native يستخدم RCTConvert خلف الكواليس لتحويل العديد من أنواع البيانات المختلفة عند نقل البيانات عبر الجسر، وسوف تعرض القيم السيئة رسائل خطأِ صندوقٍ أحمر "RedBox" لإعلامك بوجود مشكلة حالًا. وإن سار كل شيء على ما يُرام كهذه الحالة، فسيعتني هذا الماكرو بالأمر.

لتعطيل التكبير الآن، عيِّن الخاصيّة في JavaScript:

// MyApp.js
<MapView zoomEnabled={false} style={{flex: 1}} />

لتوثيق الخاصيات (والقيم التي تقبلها) لمكوّن MapView الخاص بنا، سنضيف مكونًا مُغلِّفًا (wrapper component) وسنوثّق الواجهة باستعمال خاصيّة PropTypes في React:

// MapView.js
import PropTypes from 'prop-types';
import React from 'react';
import { requireNativeComponent } from 'react-native';

class MapView extends React.Component {
  render() {
    return <RNTMap {...this.props} />;
  }
}

MapView.propTypes = {
  /**
   * A Boolean value that determines whether the user may use pinch
   * gestures to zoom in and out of the map.
   */
  zoomEnabled: PropTypes.bool
};

var RNTMap = requireNativeComponent('RNTMap', MapView);

module.exports = MapView;

لدينا الآن مكوّن مُغلَّف وموثَّق سهلُ الاستخدام. لاحظ أننا غيَّرنا معامل الدالة ‎‎requireNativeComponent‎‎ الثاني من ‎‎null‎‎ إلى المكوّن MapView المُغلِّف الجديد. يسمح هذا للبنية التحتيّة بالتحقق من أنّ أنواع propTypes تتطابق مع الخاصيات الأصيلة لتقليل فرص عدم التطابق بين شيفرة Objective-C وشيفرة JavaScript.

بعد ذلك، لِنُضِف الخاصيّة ‎‎region‎‎ الأعقَد. نبدأ بإضافة الشيفرة الأصيلة:

// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
  [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}

هذا أعقد من حالة BOOL البسيطة سابقًا. لدينا الآن نوعٌ باسم MKCoordinateRegion يحتاج إلى دالّة تحويل (conversion function)، ولدينا شيفرة مخصصّة لتحريك العرض عند ضبط المنطقة (region) من JavaScript. داخل جسم الدالّة التي نُقدِّمها، يشير json إلى القيمة الأولية (raw value) التي مُرِّرَت من JavaScript. هناك أيضًا متغيّرُ ‎‎view‎‎ يتيح لنا الوصول إلى نسخة مُعالج (أو مُدير) العرض، ونستخدم ‎‎defaultView‎‎ لإعادة تعيين الخاصية إلى القيمة الافتراضية إذا أرسلت شيفرة JavaScript القيمة ‎‎null‎‎.

يمكنك كتابة أي دالّة تحويلٍ تريدها لعَرضِك، إليك إجراء MKCoordinateRegion عبر فئةٍ (category) على RCTConvert. يستخدم هذا فئةً موجودة بالفعل من ReactNative باسم ‎‎RCTConvert+CoreLocation‎‎:

// RNTMapManager.m

#import "RCTConvert+Mapkit.h"

// RCTConvert+Mapkit.h

#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>

@interface RCTConvert (Mapkit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;

@end

@implementation RCTConvert(MapKit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
  json = [self NSDictionary:json];
  return (MKCoordinateSpan){
    [self CLLocationDegrees:json[@"latitudeDelta"]],
    [self CLLocationDegrees:json[@"longitudeDelta"]]
  };
}

+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
  return (MKCoordinateRegion){
    [self CLLocationCoordinate2D:json],
    [self MKCoordinateSpan:json]
  };
}

@end

دوال التحويل هذه مُصمَّمةٌ لمعالجة بيانات JSON بأمان، والتي قد ترميها JavaScript عبر عرض رسائل أخطاء الصندوق الأحمر وعند إرجاع قيم التهيئة القياسية (standard initialization values) عند فقدان المفاتيح أو أخطاء تطوير الأخرى.

لإكمال دعم الخاصيّة ‎‎region‎‎، نحتاج إلى توثيقها باستخدام propTypes (أو سنحصل على رسالة خطأ أن الخاصية الأصيلة غير مُوثَّقة)، بعدها يُمكننا ضبطها مثل أي خاصيّة أخرى:

// MapView.js

MapView.propTypes = {
  /**
   * A Boolean value that determines whether the user may use pinch
   * gestures to zoom in and out of the map.
   */
  zoomEnabled: PropTypes.bool,

  /**
   * The region to be displayed by the map.
   *
   * The region is defined by the center coordinates and the span of
   * coordinates to display.
   *المنطقة التي ستُعرَض في الخريطة
   *
   * تُحدَّد المنطقة عبر إحداثيات المركز ومجالها
   */
  region: PropTypes.shape({
    /** 
      * إحداثيات مركز الخريطة
     * Coordinates for the center of the map.
     */
    latitude: PropTypes.number.isRequired,
    longitude: PropTypes.number.isRequired,

    /**
     * Distance between the minimum and the maximum latitude/longitude
     * to be displayed.
     * المسافة بين الحد الأدنى والأقصى لكل من خطي العرض والطول
     */
    latitudeDelta: PropTypes.number.isRequired,
    longitudeDelta: PropTypes.number.isRequired,
  }),
};

// MyApp.js

render() {
  var region = {
    latitude: 37.48,
    longitude: -122.16,
    latitudeDelta: 0.1,
    longitudeDelta: 0.1,
  };
  return (
    <MapView
      region={region}
      zoomEnabled={false}
      style={{ flex: 1 }}
    />
  );
}

يمكنك هنا أن ترى أن شكل المنطقة واضح في توثيق JavaScript، مثاليًّا يمكننا توليد جزء من هذه الشيفرة تلقائيًّا، لكن هذه الميّزة غير متوفّرة حاليًّا.

أحيانًا، سيكون للمكوّن الأصيل بعض الخاصيات الخاصة التي لا تريد أن تكون جزءًا من واجهة برمجة التطبيقات لمكون React ذو العلاقة. مثلًا، يحتوي Switch على معالج onChange مخصَّص للحدث الأصيل الأوليّ (raw native event)، ويُوفّر الوصولَ إلى خاصيّة المعالجة onValueChange التي تُستدعى بالقيمة المنطقية فقط بدلاً من الحدث الأوليّ. ولأنّك لا تريد أن تكون هذه الخاصيات (الأصيلة حصرًا) جزءًا من واجهة برمجة التطبيقات، فأنت لا تريد وضعها في أنواع propTypes، ولكن إذا لم تفعل فستحصل على خطأ. الحل هو ببساطة إضافتها إلى الخيار ‎‎nativeOnly‎‎، مثلًا:

var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
  nativeOnly: { onChange: true }
});

الأحداث (Events)

لدينا الآن مكوّن خريطة أصيل يمكننا التحكم فيه بسهولة من JavaScript، لكن كيف نتعامل مع أحداث المستخدم (User events)، مثل القرص للتكبير أو للتصغير أو المسح (panning) لتغيير المنطقة المرئية؟

إلى الآن، قمنا فقط بإرجاع نسخة MKMapView من تابع ‎‎-(UIView *)view‎‎ الخاص بمعالج الإدارة (manager). لا يمكننا إضافة خاصيات جديدة إلى MKMapView لذلك علينا إنشاء صنف فرعيّ جديد من MKMapView، والذي سنستخدمه لعرضنا. يُمكننا بعد ذلك إضافة دالة ردّ نداءٍ (callback) باسم ‎‎onRegionChange‎‎ إلى هذا الصنف الفرعيّ:

// RNTMapView.h

#import <MapKit/MapKit.h>

#import <React/RCTComponent.h>

@interface RNTMapView: MKMapView

@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;

@end

// RNTMapView.m

#import "RNTMapView.h"

@implementation RNTMapView

@end

لاحظ أن جميع أحداث RCTBubblingEventBlock يجب أن تكون مسبوقة بالمقطع on. بعد ذلك، صرِّح (declare) عن خاصية معالجة أحداث (event handler property) على RNTMapManager، واجعلها مفوَّضةً لجميع العروض التي توفّر الوصول إليها، وأعِد توجيه الأحداث إلى JavaScript عن طريق استدعاء كتلة معالج الأحداث (event handler block) من العرض الأصيل.

// RNTMapManager.m

#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>

#import "RNTMapView.h"
#import "RCTConvert+Mapkit.h"

@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE()

RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)

RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
    [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}

- (UIView *)view
{
  RNTMapView *map = [RNTMapView new];
  map.delegate = self;
  return map;
}

#pragma mark MKMapViewDelegate

- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
  if (!mapView.onRegionChange) {
    return;
  }

  MKCoordinateRegion region = mapView.region;
  mapView.onRegionChange(@{
    @"region": @{
      @"latitude": @(region.center.latitude),
      @"longitude": @(region.center.longitude),
      @"latitudeDelta": @(region.span.latitudeDelta),
      @"longitudeDelta": @(region.span.longitudeDelta),
    }
  });
}
@end

في التابع المفوَّض ‎‎-mapView:regionDidChangeAnimated:‎‎ تُستدعى كتلة معالج الأحداث على العرض المتوافق مع بيانات المنطقة. ينتج عن استدعاء كتلة معالج الأحداث onRegionChange استدعاءُ نفس خاصيّة رد النداء في JavaScript. تُستدعى دالّة رد النداء هذه مع الحدث الأولي، والذي نقوم بمعالجته عادةً في المكوِّن المُغلِّف لتبسيط واجهة برمجة التطبيقات:

// MapView.js

class MapView extends React.Component {
  _onRegionChange = (event) => {
    if (!this.props.onRegionChange) {
      return;
    }

    // معالجة الحدث الأوليّ
    this.props.onRegionChange(event.nativeEvent);
  }
  render() {
    return (
      <RNTMap
        {...this.props}
        onRegionChange={this._onRegionChange}
      />
    );
  }
}
MapView.propTypes = {
  /**
   * Callback that is called continuously when the user is dragging the map.
 * دالة رد نداء تُستدعى باستمرار عندما يسحب المستخدم الخريطة
   */
  onRegionChange: PropTypes.func,
  ...
};

// MyApp.js

class MyApp extends React.Component {
  onRegionChange(event) {
    // Do stuff with event.region.latitude, etc.
   // تعامل مع القيمة
    // event.region.latitude
    // وغيرها من القيم
  }

  render() {
    var region = {
      latitude: 37.48,
      longitude: -122.16,
      latitudeDelta: 0.1,
      longitudeDelta: 0.1,
    };
    return (
      <MapView
        region={region}
        zoomEnabled={false}
        onRegionChange={this.onRegionChange}
      />
    );
  }
}

التعامل مع عدة عروض أصيلة

يمكن أن يكون لعرض React Native عدة أبناء في شجرة العروض، مثلاً:

<View>
  <MyNativeView />
  <MyNativeView />
  <Button />
</View>

في هذا المثال يغلف الصنف MyNativeView المكون NativeComponent ويقدم الدوال التي ستستدعى على منصة iOS. يعرَّف MyNativeView في MyNativeView.ios.js ويحوي دوال الوكيل الموجودة في NativeComponent. عندما يتفاعل المستخدم مع المكون بالنقر على الزر يتغير backgroundColor الخاص بــ MyNativeView، في هذه الحالة لن يعلم UIManager أي MyNativeView يجب أن يعالج وأي منها سيغير backgroundColor، ستحل هذه المشكلة بالطريقة التالية:

<View>
  <MyNativeView ref={this.myNativeReference} />
  <MyNativeView ref={this.myNativeReference2} />
  <Button
    onPress={() => {
      this.myNativeReference.callNativeMethod();
    }}
  />
</View>

الأى يحوي المكون مرجعًا إلى MyNativeView محدّد مما يسمح لنا باستخدام نسخة محددة من MyNativeView. الآن يستطيع الزر أن يعرف لأي MyNativeView يجب أن يغير backgroundColor .في هذا المثال لنفرض أن callNativeMethod هي التي تغير backgroundColor سيحوي MyNativeView.ios.js شيفرة كما يلي:

class MyNativeView extends React.Component {
  callNativeMethod = () => {
    UIManager.dispatchViewManagerCommand(
      ReactNative.findNodeHandle(this),
      UIManager.getViewManagerConfig('RNCMyNativeView').Commands
        .callNativeMethod,
      []
    );
  };

  render() {
    return <NativeComponent ref={NATIVE_COMPONENT_REF} />;
  }
}

callNativeMethod هي دالتنا المخصصة في iOS والتي تغير backgroundColor كمثال، والتي تعرض من خلال MyNativeView. تستخدم هذه الدالة UIManager.dispatchViewManagerCommand والتي تحتاج ثلاثة معاملات:

  • (nonnull NSNumber \*)reactTag : الرقم المعرف لعرض react.
  • commandID:(NSInteger)commandID : الرقم المعرف للدالة الأصيلةالتي يجب استدعاؤها.
  • commandArgs:(NSArray<id> \*)commandArgs : معاملات الدالة الأصيلة التي يمكن أن نمررها من JS إلى native.

RNCMyNativeViewManager.m

#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTLog.h>

RCT_EXPORT_METHOD(callNativeMethod:(nonnull NSNumber*) reactTag) {
    [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
        NativeView *view = viewRegistry[reactTag];
        if (!view || ![view isKindOfClass:[NativeView class]]) {
            RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
            return;
        }
        [view callNativeMethod];
    }];

}

تعرَّف callNativeMethod في الملف RNCMyNativeViewManager.m، وتحوي معاملًا واحدًا فقط هو (nonnull NSNumber*) reactTag. ستجد هذه الدالة المصدرة علاضًا معينًا يستخدم addUIBlock ويحوي المعامل viewRegistry ويعيد المكون اعتمادًا على reactTag مما يسمح له باستدعاء الدالة على المكون الصحيح.

الأنماط (Styles)

لأنّ جميع عروض React الأصيلة الخاصة بنا هي أصناف فرعية من الصنف UIView، فإنّ معظم سمات الأنماط (style attributes) ستعمل كالمتوقّع تلقائيًّا. لكن ستحتاج بعض المكونات إلى نمط افتراضي، مثلًا، UIDatePicker يحتاج إلى حجم ثابت. هذا النمط الافتراضي مهم لتعمل خوارزمية التخطيط كما يجب، لكننا نريد أيضًا التمكّن من الكتابة فوق النمط الافتراضي وتغييره عند استخدام المكوّن. يقوم المكوّن DatePickerIOS بهذا عن طريق تغليف المكون الأصيل في عرض إضافي يتميّز بتنسيق مرن، وباستخدام نمط ثابت (الذي يولَّد بثوابت تُمرَّر من الجزء الأصيل) على المكون الأصيل الداخلي:

// DatePickerIOS.ios.js

import { UIManager } from 'react-native';
var RCTDatePickerIOSConsts = UIManager.RCTDatePicker.Constants;
...
  render: function() {
    return (
      <View style={this.props.style}>
        <RCTDatePickerIOS
          ref={DATEPICKER}
          style={styles.rkDatePickerIOS}
          ...
        />
      </View>
    );
  }
});

var styles = StyleSheet.create({
  rkDatePickerIOS: {
    height: RCTDatePickerIOSConsts.ComponentHeight,
    width: RCTDatePickerIOSConsts.ComponentWidth,
  },
});

تُصدَّر ثوابت RCTDatePickerIOSConsts من الجزء الأصيل عبر الحصول على الإطار (frame) الفعلي للمكون الأصيل كما يلي:

// RCTDatePickerManager.m

- (NSDictionary *)constantsToExport
{
  UIDatePicker *dp = [[UIDatePicker alloc] init];
  [dp layoutIfNeeded];

  return @{
    @"ComponentHeight": @(CGRectGetHeight(dp.frame)),
    @"ComponentWidth": @(CGRectGetWidth(dp.frame)),
    @"DatePickerModes": @{
      @"time": @(UIDatePickerModeTime),
      @"date": @(UIDatePickerModeDate),
      @"datetime": @(UIDatePickerModeDateAndTime),
    }
  };
}

غطَّى هذا الدليل العديد من جوانب تجسير المكونات الأصيلة المخصصة، ولكن هناك الكثير مما قد تحتاج إلى أخذه على عين الاعتبار، مثل الخطافات المخصصة (custom hooks) لإدراج العروض الفرعية وتخطيطها. إذا أردت التعمق أكثر، انظر الشيفرة المصدريّة لبعض من مكوّنات React Native.

مصادر