التواصل بين المكون الأصيل وبين React Native في نظام iOS

من موسوعة حسوب
مراجعة 13:50، 9 أكتوبر 2021 بواسطة جميل-بيلوني (نقاش | مساهمات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

يمكنك تعلّم كيفية تضمين React Native في مكون أصيل والعكس من صفحة الدمج مع تطبيقات قائمة وصفحة مكونات واجهة المستخدم الأصيلة. عندما نمزج المكونات الأصيلة ومكونات React Native، سنجد حتما الحاجة للتواصل بين هذين العالمين. ذكرنا بعض الطرق لتحقيق ذلك في أدلّة أخرى مسبقا. يلخص هذا المقال التقنيات المتاحة.

مقدمة

React Native مستوحًى من React، وبالتالي فإن الفكرة الأساسية لتدفق المعلومات (information flow) متشابهة. التدفق في React أحادي الاتجاه. نحافظ على تسلسل هرمي للمكونات، حيث يعتمد كل مكون فقط على مكوِّنه الأب وحالته الداخلية. وذلك باستعمال الخاصيات: تُمرَّر البيانات من أحد المكونات الآباء إلى مكوناته الأبناء من أعلى إلى أسفل. إذا كان أحد مكونات الأسلاف (ancestor component) يعتمد على حالة سليله (descendant)، فيجب أن تُمرَّر دالة رد نداء (callback) ليستخدمها المكون السليل لتحديث المكون السلف.

ينطبق نفس المفهوم على React Native. طالما نقوم ببناء تطبيقنا حصريا داخل إطار العمل، يمكننا التحكم بتطبيقنا بالخاصيات ودوال رد النداء. ولكن، عندما نخلط بين مكونات React Native والمكونات الأصيلة، فإننا نحتاج إلى بعض الآليات الخاصة متعددة اللغات (cross-language) التي ستسمح لنا بتمرير المعلومات بينها.

الخاصيات

الخاصيات هي أبسط طريقة للتواصل عبر المكونات. لذلك نحتاج إلى طريقة لتمرير الخاصيات من كل من المكون الأصيل إلى React Native، ومن React Native إلى المكون الأصيل.

تمرير الخاصيات من المكون الأصيل إلى React Native

لتضمين واجهة React Native في مكون أصيل، نستخدم RCTRootView، وهو صنفٌ من النوع UIView يحمل تطبيق React Native. كما يوفر واجهة بين الجانب الأصيل والتطبيق المستضاف.

يحتوي RCTRootView على مُهيئ يسمح لك بتمرير خاصيات اعتباطيّة إلى تطبيق React Native. يجب أن يكون المعامل initialProperties نسخةً (instance) من NSDictionary. يُحوَّل القاموس داخليًا إلى كائن JSON الذي يمكن لمكون JavaScript ذي مستوى عال (top-level) الرجوع إليه.

NSArray *imageList = @[@"http://foo.com/bar1.png",
                       @"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                 moduleName:@"ImageBrowserApp"
                                          initialProperties:props];
import React from 'react';
import { View, Image } from 'react-native';

export default class ImageBrowserApp extends React.Component {
  renderImage(imgURI) {
    return <Image source={{ uri: imgURI }} />;
  }
  render() {
    return <View>{this.props.images.map(this.renderImage)}</View>;
  }
}

يوفر RCTRootView كذلك الخاصية appProperties للقراءة والكتابة (read-write property). بعد تعيين appProperties، يُعاد تصيير تطبيق React Native بخاصيات جديدة. يُنفَّذ التحديث فقط عندما تختلف الخاصيات المحدثة الجديدة عن الخاصيات السابقة.

NSArray *imageList = @[@"http://foo.com/bar3.png",
                       @"http://foo.com/bar4.png"];

rootView.appProperties = @{@"images" : imageList};

لا بأس في تحديث الخاصيات في أي وقت. ومع ذلك، يجب إجراء التحديثات على السلسلة الرئيسية. يمكنك استخدام دالة getter على أي سلسلة.

ملاحظة: هناك حاليًا مشكلة معروفة عند تعيين appProperties أثناء بدء تشغيل الجسر، إذ يمكن فقدان التغيير. انظر هذه الصفحة لمزيد من المعلومات.

لا توجد وسيلة لتحديث بعض الخاصيات فقط في كل مرة. نقترح بناءه داخل غلاف (wrapper) خاص بك بدلاً من ذلك.

ملاحظة: حاليًا، لن تُستدعَى الدالة ‎‎componentWillUpdateProps‎‎ في JavaScript لمكون React Native ذي المستوى الأعلى بعد تحديث خاصية ما. ومع ذلك، يمكنك الوصول إلى الخاصيات الجديدة داخل الدالة componentDidMount.

تمرير الخاصيات من React Native إلى المكون الأصيل

تمّت تغطية مشكلة توفير الوصول لخاصيات المكونات الأصيلة بالتفصيل في هذه الصفحة. باختصار، صدِّر (export) الخاصيات باستخدام ماكرو RCT_CUSTOM_VIEW_PROPERTY في مكونك الأصيل المخصص، ثم استخدمها في React Native كما لو كان المكون مُجرَّد مكونِ React Native عادي.

حدود الخاصيات

العيب الرئيسي للخاصيات متعددة اللغات هو أنها لا تدعم دوال رد النداء، والتي تسمح لنا بمعالجة ارتباطات البيانات من أسفل إلى أعلى. تخيل أن لديك واجهة React Native صغيرة تريد إزالتها من واجهة المكون الأصيل الأب كنتيجة لإجراءٍ في JavaScript. لا توجد طريقة للقيام بذلك باستخدام الخاصيات، لأن المعلومات ستحتاج إلى الانتقال من الأسفل إلى الأعلى.

رغم أن لدينا نوع من أنواع دوال رد النداء متعددة اللغات (الموصوفة هنا)، إلا أن دوال رد النداء هذه ليست دائمًا ما نحتاجه. المشكلة الرئيسية هي أنها غير مُصمَّمةٍ لتُمرَّر كخاصيات. بدلاً من ذلك، تسمح لنا هذه الآلية بتشغيل إجراء أصيل من JavaScript، والتعامل مع نتيجة هذا الإجراء في JavaScript.

طرق أخرى للتفاعلات متعددة اللغات (الأحداث والوحدات الأصيلة)

كما هو مذكور في الفصل السابق، يأتي استخدام الخاصيات مع بعض القيود. أحيانًا لا تكفي الخاصيات للتحكم بمنطق تطبيقنا، ونحتاج حلا يمنح مزيدًا من المرونة. يغطي هذا الفصل تقنيات تواصل أخرى متاحة في React Native. يمكن استخدامها للتواصل الداخلي (بين JavaScript والطبقات الأصيلة في React Native) وكذلك للتواصل الخارجي (بين React Native والجزء "الأصيل الخالص" من تطبيقك).

يمكّنك React Native من إجراء استدعاءات الدوال متعددة اللغات. يمكنك تنفيذ شيفرة أصيلة مخصصة من JavaScript والعكس. لسوء الحظ، بناءً على الجانب الذي نعمل عليه، نحقق نفس الهدف بطرق مختلفة. بالنسبة للجهة الأصيلة - نستخدم آلية الأحداث لجدولة تنفيذ دالة معالِجةٍ (handler function) في JavaScript، بينما بالنسبة لإطار React Native، فإننا نستدعي مباشرةً التوابع المصدَّرة من الوحدات الأصيلة.

استدعاء دوال React Native من المكون الأصيل (الأحداث)

وُصفت الأحداث بالتفصيل في هذه الصفحة. لاحظ أن استخدام الأحداث لا يمنحنا أي ضمانات بشأن وقت التنفيذ (execution time)، إذ يعالج الحدث في سلسلة منفصلة.

الأحداث مفيدة جدًّا، لأنها تسمح لنا بتغيير مكونات React Native دون الحاجة إلى أي مرجع يشير إليها. ومع ذلك، هناك بعض المشاكل التي قد تقع فيها أثناء استخدامها:

  • لأنه يمكن إرسال الأحداث من أي مكان، فيمكنها إدخال تبعيات متشابكة -بشكل مربك- داخل مشروعك.
  • تشترك الأحداث في مجال الأسماء (namespace)، مما يعني أنك قد تواجه بعض تضارب الأسماء. لن تُكتشف التضاربات بشكل ساكن (statically)، مما يجعلها صعبة التنقيح (debug).
  • إذا كنت تستخدم عدة نسخ لنفس مكون React Native وتريد تمييزها من منظور الحدث الخاص بك، فستحتاج على الأرجح إلى تقديم معرّفات (identifiers) وتمريرها مع الأحداث (يمكنك استخدام وسم reactTag الخاص بالواجهة الأصيلة كمعرّف) .

يتمثل النمط الشائع الذي نستخدمه عند تضمين المكون الأصيل في React Native في جعل RCTViewManager للمكون الأصيل مفوَّضًا للواجهات، وإرسال الأحداث مرة أخرى إلى JavaScript عبر الجسر. هذا يُبقي استدعاءات الحدث ذات الصلة في مكان واحد.

استدعاء الدوال الأصيلة من React Native (الوحدات الأصيلة)

الوحدات الأصيلة هي أصناف Objective-C المتوفرة في JavaScript. تُنشأُ نسخة وحدة واحدة في كل جسر JavaScript. يمكنها تصدير دوال وثوابت اعتباطيّة إلى React Native. تمّت تغطيتها بالتفصيل في هذه الصفحة.

الوحدات الأصيلة هي وحداتٌ مفردة (singletons) ما يحدّ من الآلية (mechanism) في سياق التضمين (embedding). لنقل أن لدينا مكون React Native مُضمَّن في واجهة أصيلة ونريد تحديث الواجهة الأب الأصيلة. باستخدام آلية الوحدة الأصيلة، سنُصدِّر دالةً لا تأخذ المعاملات المتوقعة فحسب، بل وأيضًا مُعرّفًا للواجهة الأب الأصيلة. سيُستخدم المعرِّف لاسترداد مرجعِِ (reference) إلى الواجهة الأب التي نريد تحديثها. ومع ذلك، سنحتاج إلى الاحتفاظ بارتباطٍ (mapping) من المعرِّفات إلى الواجهات الأصيلة في الوحدة.

رغم أن هذا الحل معقد، إلا أنه يُستخدَم في RCTUIManager، وهو صنف React Native داخليّ يُدير جميع واجهات React Native.

يمكن أيضًا استخدام الوحدات الأصيلة لتوفير الوصول إلى المكتبات الأصيلة الموجودة مسبقا للغة JavaScript. مكتبة تحديد الموقع الجغرافي Geolocation library مثال للفكرة.

تحذير: تشترك كافة الوحدات الأصيلة في نفس مجال الأسماء. احذر من تضارب الأسماء عند إنشاء وحدات جديدة.

تدفق حساب التخطيط (Layout computation flow)

عند دمج المكونات الأصيلة وReact Native، نحتاج أيضًا إلى طريقة لدمج نظامي تخطيط (layout systems) مختلفين. يغطي هذا القسم مشاكل التخطيط الشائعة ويوفر وصفًا موجزًا للآليات اللازمة لمعالجتها.

تخطيط مكون أصيل مضمّن في React Native

غطّينا هذه الحالة في هذه الصفحة. نظرًا لأن جميع واجهات React الأصيلة لدينا هي أصناف فرعية من الصنف UIView، فمعظم سمات النمط والحجم (style and size) ستعمل كالمتوقع افتراضيا.

تخطيط مكون React Native مضمّن في المكون الأصيل

محتوى React Native ذو حجم ثابت

أبسط سيناريو هو عندما يكون لدينا تطبيق React Native بحجم ثابتٍ معروفٍ عند الجانب الأصيل. على وجه الخصوص، تقع واجهة React Native بوضع ملء الشاشة في هذه الدائرة. إذا أردنا واجهةً جذرًا أصغر، فيمكننا تعيين إطار RCTRootView بشكل صريح.

على سبيل المثال، لجعل تطبيق React Native بارتفاع 200 بكسل (منطقي)، و اتّساع (width) الواجهة (view) المضيف، يمكننا القيام بما يلي:

// SomeViewController.m

- (void)viewDidLoad
{
  [...]
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:appName
                                            initialProperties:props];
  rootView.frame = CGRectMake(0, 0, self.view.width, 200);
  [self.view addSubview:rootView];
}

عندما يكون لدينا واجهة رئيسية (جذر root) بحجمٍ ثابتٍ، لابد من احترام حدوده على جانب JavaScript. بمعنى آخر، نحتاج إلى التأكد من إمكانية احتواء محتوى React Native في الواجهة الجذر ذي الحجم الثابت. أسهل طريقة لضمان ذلك هي استخدام تخطيط flexbox. إذا استخدمت التموضع المطلق (absolute positioning)، وكانت مكونات React مرئية خارج حدود الواجهة الجذر، فستحصل على تداخل مع واجهات المكون الأصيل، مما يؤدي إلى سلوك غير متوقع لبعض الميزات. على سبيل المثال، لن يُبرز TouchableHighlight لمساتك خارج حدود الواجهة الجذر.

لا بأس بتحديث حجم الواجهة الجذر بشكل ديناميكي عن طريق إعادة ضبط خاصية إطاره (frame property). سيهتم React Native بتخطيط المحتوى.

محتوى React Native بحجم مرن

في بعض الحالات، نود تصيير محتوى بحجم غير معروف مبدئيا. لنفترض أن الحجم سيُحدَّد ديناميكيًّا في JavaScript. لدينا حلان لهذه المشكلة:

  1. يمكنك تغليف واجهة React Native الخاص بك في مكون ScrollView. هذا يضمن أن محتواك سيكون متاحًا دائمًا ولن يتداخل مع الواجهات الأصيلة.
  2. يتيح لك React Native تحديد حجم تطبيق React Native في JavaScript وتزويده لمالك الصنف RCTRootView المُضيف. المالك هو المسؤول عن إعادة تخطيط الواجهات الفرعية والحفاظ على توافق واجهة المستخدم. نحقق هذا من خلال أوضاع مرونة (flexibility modes) الصنف RCTRootView.

يدعم RCTRootView أربعة أوضاع مرونة مختلفة الحجم:

// RCTRootView.h

typedef NS_ENUM(NSInteger, RCTRootViewSizeFlexibility) {
  RCTRootViewSizeFlexibilityNone = 0,
  RCTRootViewSizeFlexibilityWidth,
  RCTRootViewSizeFlexibilityHeight,
  RCTRootViewSizeFlexibilityWidthAndHeight,
};

القيمة RCTRootViewSizeFlexibilityNone هي الافتراضية، والتي تجعل حجم واجهةٍ جذرٍ ثابتًا (مع إمكانية تحديثه باستخدام ‎‎setFrame:‎‎). تسمح لنا الأوضاع الثلاثة الأخرى بتتبع تحديثات حجم محتوى React Native. على سبيل المثال، سيؤدي تعيين الوضع إلى RCTRootViewSizeFlexibilityHeight إلى جعل React Native يقيس ارتفاع المحتوى وتمرير هذه المعلومات إلى مفوَّض RCTRootView. يمكن تنفيذ إجراء اعتباطيّ داخل المفوَّض، بما في ذلك تعيين إطار الواجهة الجذر، بحيث يتناسب المحتوى. يُستدعى المفوَّض فقط عندما يتغير حجم المحتوى.

تحذير: يؤدي جعل بُعدِِ مرنًا في كل من JavaScript والمكون الأصيل إلى سلوك غير محدد. على سبيل المثال، لا تجعل واجهة مكون React ذو مستوى عالٍ مرنًا (مع flexbox) أثناء استخدام RCTRootViewSizeFlexibilityWidth على RCTRootView المستضيف.

مثال:

// FlexibleSizeExampleView.m

- (instancetype)initWithFrame:(CGRect)frame
{
  [...]

  _rootView = [[RCTRootView alloc] initWithBridge:bridge
  moduleName:@"FlexibilityExampleApp"
  initialProperties:@{}];

  _rootView.delegate = self;
  _rootView.sizeFlexibility = RCTRootViewSizeFlexibilityHeight;
  _rootView.frame = CGRectMake(0, 0, self.frame.size.width, 0);
}

#pragma mark - RCTRootViewDelegate
- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView
{
  CGRect newFrame = rootView.frame;
  newFrame.size = rootView.intrinsicContentSize;

  rootView.frame = newFrame;
}

في المثال، لدينا واجهة FlexibleSizeExampleView تحتوي على واجهةٍ جذرٍ. نقوم بإنشاء الواجهة الجذر وتهيئتها وتعيين المفوَّض. سيقوم المفوَّض بمعالجة تحديثات الحجم. بعد ذلك، نقوم بتعيين مرونة حجم الواجهة الجذر على RCTRootViewSizeFlexibilityHeight، مما يعني أنه سيتم استدعاء التابع ‎‎rootViewDidChangeIntrinsicSize:‎‎ في كل مرة يُغيِّر فيها محتوى React Native ارتفاعه. أخيرًا، نعيّن اتّساع أو عرض (width) الواجهة الجذر وموضعها. لاحظ أننا حددنا ارتفاعه أيضًا، لكن ليس له أي تأثير لأنّنا جعلنا الارتفاع معتمداً على React Native.

يمكنك تفقد شيفرة المصدر الكاملة للمثال هنا.

لا بأس في تغيير وضع مرونة حجم الواجهة الجذر بشكل ديناميكي. سيؤدي تغيير وضع مرونة الواجهة الجذر إلى جدولة إعادة حساب تخطيطٍ (layout recalculation) و سيُستدعَى التابع المفوَّض ‎‎rootViewDidChangeIntrinsicSize:‎‎ بمجرد معرفة حجم المحتوى.

ملاحظة: يُنفَّذ حساب تخطيط React Native في خيط (thread) خاص، في حين أن تحديثات واجهة المستخدم UI للواجهة الأصيلة (native view) تؤدّى في الخيط الرئيسي (main thread). قد يتسبب هذا في عدم تناسق مؤقت في واجهة المستخدم بين الجهة الأصيلة وجهة React Native. هذه مشكلة معروفة ويعمل فريق React Native على مزامنة تحديثات واجهة المستخدم الواردة من مصادر مختلفة.

ملاحظة: لا يُنفِّذ React Native أي حسابات تخطيط حتى تصبح الواجهة الجذر الرئيسية واجهةً فرعيةً (subview) لبعض الواجهات الأخرى. إذا كنت تريد إخفاء واجهة React Native حتى تُعرف أبعادها، فأضف الواجهة الجذر كواجهة فرعية واجعلها مخفيةً مبدئيًّا (استخدم خاصية hidden الخاصة بالصنف UIView). ثم غيّر قابلية رؤيته (visibility) في التابع المفوَّض.

مصادر