آموزشگاه برنامه نویسی تحلیل داده
آموزشگاه برنامه نویسی تحلیل داده

آموزش native component ها برای طراحی رابط کاربری در React Native

دوره های مرتبط با این مقاله

آموزش native component ها برای طراحی رابط کاربری در React Native (مخصوص سیستم عامل iOS)

هزاران native component وجود دارد، بعضی بخشی از پلتفرم هستند، بعضی در کتابخانه های خارجی قابل دسترس هستند، و حتی بعضی در کار شما استفاده شده اند. React Native اغلب component های کلیدی را در اختیار شما می گذارد، مثل ScrollView و TextInput، اما گاهی به component هایی نیاز داریم که در React Nativeنیست. بعلاوه ممکن است خودتان component هایی برای application تان ساخته باشید که بخواهید از آن ها استفاده کنید. خوشبختانه به سادگی می توان این component های موجود را درون اپلیکیشن React Native استفاده کرد.

این بخش راهنمایی پیشرفته است و فرض بر این است که با برنامه نویسی iOS آشنا هستید. به شما می آموزیم چگونه یک component native بسازید و برای این کار مثالی از component MapView می آوریم که در کتابخانه React Native وجود دارد.

مثال

فرض کنید می خواهیم یک نقشه تعاملی به application خود اضافه کنیم. می توانیم از MKMapView استفاده کنیم، کافی است آن را در جاوااسکریپت قابل دسترس داشته باشیم.

native view ها از طریق زیر کلاس های RCTViewManager ساخته می شوند و قابل تغییرند. عملکرد این زیرکلاس ها شبیه view کنترلرهاست، اما singleton هستند – فقط یک نمونه از هرکدام توسط bridge ساخته می شود. آن ها native view ها را در اختیار RCTUIManager می گذارند، که درنهایت مقداردهی و تغییر property های view را به خودشان واگذار می کنند. این RCTViewManager ها در واقع نقش delegate برای viewها را دارند، و eventها را از طریق bridge به جاوااسکریپت می فرستند.

برای اینکه به یک view در جاوااسکریپت دسترسی داشته باشیم:


  • برای ساخت یک manager برای component، از RCTViewManager یک زیرکلاس می گیریم.
  • RCT_EXPORT_MODULE() marker macro را اضافه کنیم.
  • متد -(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

توجه: property های frame یا backgroundColor را روی نمونه ی UIView مقداردهی نکنید. React Native مقادیر را طوری جایگزین می کند که با property های layout component تان هم خوانی داشته باشد. اگر حتما این میزان کنترل را می خواهید، بهتر است نمونه ی که می خواهید استایل دهی کنید را درون یک UIView دیگر قرار دهید و آن را برگردانید. برای مطالعه ی بیشتر Issue 2948 را ببینید.

در مثال بالا، نام کلاس را با پیشوند RNT انتخاب کردیم. پیشوندها برای اجتناب از مغایرت بین نام ها با framework های دیگر استفاده می شود. framework اپل از پیشوندهای دو حرفی استفاده می کند. و React Native از RCT به عنوان پیشوند استفاده می کند. برای اجتناب از هرگونه مغایرتی بین نام ها، از پیشوندی سه حرفی غیر از RCT برای کلاس هایتان استفاده کنید.

حال برای استفاده از این component در جاوااسکریپت:


// 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 استفاده کنید. می خواهیم manager را اینجا require کنیم، تا بتوانیم view manager را در جاوااسکریپت استفاده کنیم.

نکته: هنگام render کردن، view را stretch کنید، در غیر این صورت صفحه درست نمایش داده نمی شود.


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

حال یک component کاملا native در جاوااسکریپت داریم و تمام حرکت هایی که viewهای native از آن ها پشتیبانی می کنند مثلا pinch-zoom را داریم، اما هنوز نمی توانیم آن را در جاوااسکریپت کنترل کنیم.

property ها

برای کار با این component، اولین کاری که می توانیم انجام دهیم این است که property هایی به آن بدهیم. فرض کنید می خواهیم zoom را غیرفعال کنیم و region قابل مشاهده را مشخص کنیم. غیرفعال کردن zoom با افزودن یک Boolean ساده انجام می شود:


// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

توجه کنید که نوع را مشخصا Boolean تعیین کردیم. React Native از RCTConvert برای تبدیل انوع داده های مختلف هنگام ارتباط روی bridge استفاده می کند، و برای مقادیر نامناسب خطایی نشان می دهد تا شما را از مشکل آگاه کند.

حال برای اینکه واقعا zoom را غیر فعال کنیم، در جاوااسکریپت property را اینگونه مقداردهی می کنیم:


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

برای مستند کردن property های component MapView، و اینکه چه مقادیری می پذیرند، یک component wrapper برایش استفاده می کنیم، و interface را با استفاده از PropTypes مستند می کنیم:


// 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 تغییر داده ایم. به این شکل زیرساخت میتواند با هدف کم کردن امکان ناهمخوانی ها بین جاوااسکریپت و Objective-C، سازگاری propTypes ها با property های native را بررسی کند.

حال بیایید property region را اضافه کنیم. ابتدا کد native برای آن می نویسیم:


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

این پیچیده تر از کد قبلی به نظر می رسد. یک نوع MKCoordinateRegion دارید که یک تابع تبدیل لازم دارد، و کد سفارشی داریم که هنگام مقداردهی به region از جاوااسکریپت، view را animate می کند. درون بدنه متد، json به مقداری که از جاوااسکریپت ارسال شده اشاره می کند. یک متغیر view هم هست که به ما امکان دسترسی به نمونه manager می دهد، و defaultView که برای بازگرداندن property به مقادیر پیش فرض استفاده می شود (اگر JS برای ما null فرستاده باشد).

می توانید هر تابع تبدیلی برای view خود بنویسید – اینجا پیاده سازی MKCoordinateRegion را ببینید که از RCTConvert+CoreLocation آماده به عنوان category استفاده می کند:


// RNTMapManager.m
#import "RCTConvert+Mapkit.m"
// 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 که جاوااسکریپت به آن ها پاس دهد طراحی شده اند. در صورت نبود مقادیر یا خطاهای دیگر برنامه نویس، او را آگاه می کند و مقادیر استاندارد اولیه را برمی گرداند.

برای اتمام این بخش، باید آن را در propTypes مستند کنیم (در غیر این صورت خطای the native prop is undocumented می گیریم). پس از آن می توانیم region را مثل همه property های دیگر مقداردهی کنیم:


// 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 } } />
  );
}

گاهی native Component شما property های خاصی دارد که نمی خواهید بخشی از API component React Native مربوطه باشد. برای مثال، Switch برای eventهای native یک handler به نام onChange دارد وonValueChange handler property دارد که به جای فراخوانی از طریق یک event، فقط با یک مقدار بولین فراخوانی می شود. نمی خواهیم آن ها را در propTypes بگذاریم چرا که نمی خواهیم بخشی از API باشد، اما اگر این کار را نکنیم، خطا خواهیم گرفت. راه حل افزودن آن ها به nativeOnly است:


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

event ها

حال یک component نقشه native داریم که می توانیم به راحتی آن را از طریق JS کنترل کنیم. اما چطور با تعاملات کاربر مثل pinch-zoom یا panning برای تغییر region قابل مشاهده کار کنیم؟

تا این لحظه فقط یک نمونه از MKMapView از متد -(UIView *)view برگردانده ایم. نمی توانیم property های جدید به MKMapView اضافه کنیم پس باید زیرکلاس جدیدی از MKMapView بسازیم و برای view استفاده کنیم. سپس می توانیم یک onRegionChange callback روی این زیرکلاس اضافه کنیم:


// 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 داشته باشند. سپس یک event handler property روی RNTMapManager تعریف کنید، آن را یک delegate برای تمام viewهایی که به آن ها دسترسی می دهد قرار دهید و event ها را با فراخوانی handler از native به JS بفرستید.


// RNTMapManager.m
#import < MapKit/MapKit.h>
#import < React/RCTViewManager.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.m"
@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

در متد delegate -mapView:regionDidChangeAnimated: بلوک event handler روی view مربوطه با دیتای region فراخوانی می شود. فراخوانی onRegionChange باعث فراخوانی callback prop مربوطه در جاوااسکریپت می شود. این callback با event فراخوانی می شود، که ما برای ساده نگه داشتن API، آن را در یک wrapper component پردازش می کنیم:


// MapView.js
class MapView extends React.Component {
  _onRegionChange = (event) => {
    if (!this.props.onRegionChange) {
      return;
    }
    // process raw event...
    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.
  }
  render() {
    var region = {
      latitude: 37.48,
      longitude: -122.16,
      latitudeDelta: 0.1,
      longitudeDelta: 0.1,
    };
    return (
      < MapView
        region={region}
        zoomEnabled={false}
        onRegionChange={this.onRegionChange}
      />
    );
  }
}

مدیریت چند native view

یک view React Native می تواند بیش از یک view child داشته باشد. مثلا:


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

در این مثلا، کلاس MyNativeView نقش wrapper برای NativeComponent را دارد و به متدهایی دسترسی می دهد که روی iOS فراخوانی می شود. MyNativeView در MyNativeView.ios.js تعریف شده و متدهای NativeComponent را دارد.

وقتی کاربر با component کار می کند، مثلا روی دکمه ای کلیک می کند، property backgroundColor از MyNativeView تغییر می کند. UIManager نمی داند کدام MyNativeView باید هندل شود و کدام باید رنگ backgroundرا تغییر دهد. در زیر راهی برای این مشکل ارائه می کنیم:


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

مثال بالا رفرنسی به یک MyNativeView خاص دارد که به ما امکان استفاده از یک نمونه خاص از آن را می دهد. حال دکمه می تواند تعیین کند کدام MyNativeView باید رنگ backgroundرا تغییر دهد. در این مثال، فرض می کنیم callNativeMethod رنگ background را تغییر می دهد.

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 *) reacTag - شناسه view در react
  • commandID: (NSInteger) commandID - شناسه متد native که باید فراخوانی شود
  • commandArgs: (NSArray *) commandArgs - Args آرگومان های متد native که می توان از JS به native ارسال کرد.

#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 را داراست. این متد یک viewی خاص را با استفاده از addUIBlock پیدا می کند که حاوی پارامتر viewRegistry است و component را براساس reactTag برمی گرداند، و به آن امکان فراخوانی متد روی component مناسب را می دهد.

استایل ها

از آن جا که همه ی viewهای native زیرکلاسی از UIView هستند، اغلب attribute های استایل دهی همان طور که انتظار داریم رفتار می کنند. با این حال بعضی component ها استایل می خواهند، مثلا UIDatePicker سایزی ثابت دارد. این استایل پیش فرض برای اینکه الگوریتم layout درست کار کند مهم است، اما باید بتوان مقدار پیش فرض را تغییر داد. DatePickerIOS این کار را با قرار دادن component native درون یک view دیگر که استایل دهی انعطاف پذیری دارد، ممکن می کند. و view درونی آن یک استایل ثابت خواهد داشت (که از کد native ارسال شده):


// 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 واقعی native Component به شکل زیر از native منتقل می شوند:


// 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),
    }
  };
}

در این آموزش جنبه های مختلف استفاده از native Component ها را بررسی کردیم، اما موارد زیادی است که باید به آن توجه شود. اگر می خواهید عمیق تر به این موضوع بپردازید، این Source Code را ببینید.

  • 381
  •    0
  • تاریخ ارسال :   1398/06/10

دانشجویان گرامی اگر این مطلب برای شما مفید بود لطفا ما را در GooglePlus محبوب کنید
رمز عبور: tahlildadeh.com یا www.tahlildadeh.com
ارسال دیدگاه نظرات کاربران
شماره موبایل دیدگاه
عنوان پست الکترونیک

ارسال

آموزشگاه برنامه نویسی تحلیل داده
آموزشگاه برنامه نویسی تحلیل داده

تمامی حقوق این سایت متعلق به آموزشگاه تحلیل داده می باشد .