美文网首页跨平台
React Native之在js中使用原生模块、UI组件(iOS

React Native之在js中使用原生模块、UI组件(iOS

作者: 平安喜乐698 | 来源:发表于2019-02-14 15:39 被阅读10次
目录

  1. 访问iOS原生模块
  2. 访问iOS原生UI组件
  3. 链接原生库
1. 访问iOS原生模块
React Native支持访问原生平台的功能,建议不应经常出现。

<1>创建类。遵守RCTBridgeModule协议,在.m文件中添加宏。

1、向React Native暴露该模块
  // 参数:自定义在React Native中访问该模块的名字,默认类名,如果类名以 RCT 开头则自动移除这个前缀
  // RCT_EXPORT_MODULE(AwesomeCalendarManager); 
  RCT_EXPORT_MODULE()


2、向React Native暴露该模块的方法
  // 异步的,想传值给js必须使用回调或事件
  // 导出到 JavaScript 的方法名是 Objective-C 的方法名的第一个部分
  // 返回值类型必须是void
  RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details)
  {
    NSString *location = [RCTConvert NSString:details[@"location"]];
    NSDate *time = [RCTConvert NSDate:details[@"time"]];
    ...
  }

3、当2个方法名冲突时使用 
  RCT_REMAP_METHOD

RCT_EXPORT_METHOD 详细说明

1、支持所有标准 JSON 类型,包括:
  string (对应 NSString)
  number (对应 NSInteger, float, double, CGFloat, NSNumber)
  boolean (对应 BOOL, NSNumber)
  array (对应 NSArray) 任意类型
  object (对应 NSDictionary) 可包含 string 类型的键和任意类型的值
  function (对应 RCTResponseSenderBlock) 回调函数,接受一个数组参数
2、RCTConvert类支持的的类型。
  RCTConvert提供了一系列辅助函数,用来接收一个 JSON 值并转换到原生 Objective-C 类型或类。
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}
或
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}
或(这里会自动进行类型转换)
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date)
{
  // Date is ready to use!
}

========================
======== 分别对应 =======
========================

CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.getTime(),
); // 把日期以unix时间戳形式传递
和
CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey',
  date.toISOString(),
); // 把日期以ISO-8601的字符串形式传递
和
1或2都可以
RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback)
{
  NSArray *events = ...
  callback(@[[NSNull null], events]);
}

========================
======== 对应 =======
========================

CalendarManager.findEvents((error, events) => {
  if (error) {
    console.error(error);
  } else {
    this.setState({events: events});
  }
});

示例

// CalendarManager.h
#import <React/RCTBridgeModule.h>
@interface CalendarManager : NSObject <RCTBridgeModule>
@end

// CalendarManager.m
#import "CalendarManager.h"
#import <React/RCTLog.h>
@implementation CalendarManager
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(getName:(NSString *)name sex:(NSString *)sex){
  RCTLogInfo(@"%@  %@", name, sex);
}
@end

<2> js中调用

import {NativeModules} from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.getName('hello', 'world');

<3> 其他相关知识

Promises简化代码

如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock,则对应的 JS 方法就会返回一个 Promise 对象


RCT_REMAP_METHOD(findEvents
                 findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  NSArray *events = ...
  if (events) {
    resolve(events);
  } else {
    NSError *error = ...
    reject(@"no_events", @"There were no events", error);
  }
}
对应
async function updateEvents() {
  try {
    var events = await CalendarManager.findEvents();

    this.setState({events});
  } catch (e) {
    console.error(e);
  }
}
updateEvents();

多线程

React Native 默认在一个独立的串行 GCD 队列中调用原生模块的方法,指定想在哪个队列中执行有2种方式:
  1、通过实现methodQueue方法来,会影响所有方法
  - (dispatch_queue_t)methodQueue{
    return dispatch_get_main_queue();
  }
  2、在相应法法中使用GCD
  RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
  {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      // 在这里执行长时间的操作
      ...
      // 你可以在任何线程/队列中执行回调函数
      callback(@[...]);
    });
  }

若干个模块中共享同一个队列,仅仅是返回相同名字的队列是不行的,必须返回同一个队列实例。

注入依赖

bridge 会自动注册实现了RCTBridgeModule协议的模块,但是你可能也希望能够初始化自定义的模块实例(这样可以注入依赖)

  id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];
  RCTRootView *rootView = [[RCTRootView alloc]
                        initWithBridge:bridge
                            moduleName:kModuleName
                     initialProperties:nil];

导出常量

原生模块可以导出一些常量,在 JavaScript端随访问。仅仅在初始化的时候导出了一次。

实现constantsToExport方法
  - (NSDictionary *)constantsToExport{
    return @{ @"firstDayOfTheWeek": @"Monday" };
  }
JavaScript访问:
  console.log(CalendarManager.firstDayOfTheWeek);
枚举常量

创建RCTConvert分类,实现constantsToExport方法

如果希望导出
  typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {
    UIStatusBarAnimationNone,
    UIStatusBarAnimationFade,
    UIStatusBarAnimationSlide,
  };
则创建RCTConvert分类
  @implementation RCTConvert (StatusBarAnimation)
  RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
                                               @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
                                               @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}),
                      UIStatusBarAnimationNone, integerValue)
  @end
实现constantsToExport方法
  - (NSDictionary *)constantsToExport
  {
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) };
  };
使用
  RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)

向js发送事件

即使没有被 JavaScript 调用,原生模块也可以给 JavaScript 发送事件通知。


// CalendarManager.h
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface CalendarManager : RCTEventEmitter <RCTBridgeModule>
@end

// CalendarManager.m
#import "CalendarManager.h"
@implementation CalendarManager
RCT_EXPORT_MODULE();
- (NSArray<NSString *> *)supportedEvents
{
  return @[@"EventReminder"];
}
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  NSString *eventName = notification.userInfo[@"name"];
  [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
}
@end


JS
import { NativeEventEmitter, NativeModules } from 'react-native';
const { CalendarManager } = NativeModules;
const calendarManagerEmitter = new NativeEventEmitter(CalendarManager);
const subscription = calendarManagerEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...
// 别忘了取消订阅,通常在componentWillUnmount生命周期方法中实现。
subscription.remove();
优化无监听处理的事件

如果你发送了一个事件却没有任何监听处理,则会因此收到一个资源警告。要优化因此带来的额外开销,你可以在你的RCTEventEmitter子类中覆盖startObserving和stopObserving方法。


  @implementation CalendarManager
  {
    bool hasListeners;
  }
  // 在添加第一个监听函数时触发
  -(void)startObserving {
    hasListeners = YES;
    // Set up any upstream listeners or background tasks as necessary
  }

  // Will be called when this module's last listener is removed, or on dealloc.
  -(void)stopObserving {
    hasListeners = NO;
    // Remove upstream listeners, stop unnecessary background tasks
  }

  - (void)calendarEventReminderReceived:(NSNotification *)notification
  {
    NSString *eventName = notification.userInfo[@"name"];
    if (hasListeners) { // Only send events if anyone is listening
      [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}];
    }
  }
2. 访问iOS原生UI组件

步骤

1、创建一个RCTViewManager的子类。
  原生视图都需要被一个RCTViewManager的子类来创建和管理,是个单例。
2、添加RCT_EXPORT_MODULE()宏标记。
3、实现-(UIView *)view方法。
  不要在-view中给UIView实例设置frame或是backgroundColor属性。为了和 JavaScript 端的布局属性一致,React Native 会覆盖你所设置的值

示例

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

@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
- (UIView *)view{
  return [[MKMapView alloc] init];
}
@end



// MapView.js
import { requireNativeComponent } from 'react-native';
// requireNativeComponent 自动把'RNTMap'解析为'RNTMapManager'
export default requireNativeComponent('RNTMap', null);

// MyApp.js
import MapView from './MapView.js';
render() {
  return <MapView style={{ flex: 1 }} />;
}

属性

// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

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

优化(.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 = {
  /**
   * 
   */
  zoomEnabled: PropTypes.bool,
};
// 第二个参数使得 React Native 的底层框架可以检查原生属性和包装类的属性是否一致,来减少出现问题的可能。
var RNTMap = requireNativeComponent('RNTMap', MapView);
export default MapView;

例(复杂)

RNTMapManager中+
  #import "RCTConvert+Mapkit.h"
  RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
  {
    // 类型转换(创建RCTConvert的分类)
    [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
  }

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
RCTConvert+Mapkit.m
  #import "RCTConvert+Mapkit.h"
  @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

MapView.js的MapView.propTypes中+
  /**
   * 地图要显示的区域。
   *
   * 区域由中心点坐标和区域范围坐标来定义。
   *
   */
  region: PropTypes.shape({
    /**
     * 地图中心点的坐标。
     */
    latitude: PropTypes.number.isRequired,
    longitude: PropTypes.number.isRequired,

    /**
     * 最小/最大经、纬度间的距离。
     *
     */
    latitudeDelta: PropTypes.number.isRequired,
    longitudeDelta: PropTypes.number.isRequired,
  }),

App.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 }}
      />
    );
  }
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
  nativeOnly: {onChange: true},
});

事件

首先需要创建一个属性(用于回调)

因为MKMapView是系统类,不能添加属性,所以创建一个 MKMapView 的子类

// RNTMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMapView: MKMapView
// 所有 RCTBubblingEventBlock 必须以 on 开头
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end

// RNTMapView.m
#import "RNTMapView.h"
@implementation RNTMapView
@end

在RCTViewManager中暴露属性,按需调用

// 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

样式

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


// 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,
  },
});
3. 链接原生库(若js的某个库带有原生依赖)

自动链接

npm install 某个带有原生依赖的库
  安装一个带原生依赖的库

react-native link
  链接所有需要链接的库(根据package.json文件中的dependencies和devDependencies记录)
【或】
react-native link 指定库名
  链接指定库

手动链接

1、如果该库包含原生代码,那么在它的文件夹下一定有一个.xcodeproj文件。把这个文件拖到你的 XCode 工程下(通常拖到 XCode 的Libraries分组里)
2、Build Phases | Link Binary With Libraries 添加.xcodeproj文件中的.a静态文件
3、大部分情况忽略此步,两个例外:PushNotificationIOS和LinkingIOS
  Build Settings | Header Search Paths 添加库所在目录

相关文章

网友评论

    本文标题:React Native之在js中使用原生模块、UI组件(iOS

    本文链接:https://www.haomeiwen.com/subject/kedceqtx.html