8) React Native 与原生之间的通信(iOS)

js and native.png

本文将讲述下在原生和React Native之间的通信方式。方式和逻辑综合了自己的思维方式,主要参考了React Native中文官方文档,因为感觉它讲的方式有些不妥,所以就按自己思路组织了下文。
虽然发觉一遍文章要把所有通信方式讲清楚不太科学,不过把思路讲讲倒是可以,总体思路是,原生和React Native之间的通信方式主要包括三大部分:

  • 属性
  • 原生模块
  • 原生UI组件封装

(下文主要详细讲了前两部分,最后一部分还在研究,等待更新中)

一、属性

React Native是从React中得到的灵感,因此基本的信息流是类似的。在React中信息是单向的。我们维护了组件层次,在其中每个组件都仅依赖于它父组件和自己的状态。通过属性(properties)我们将信息从上而下的从父组件传递到子元素。如果一个祖先组件需要自己子孙的状态,推荐的方法是传递一个回调函数给对应的子元素。
属性是最简单的跨组件通信。因此我们需要一个方法从原生组件传递属性到React Native或者从React Native到原生组件。

原生给JS传数据,主要依靠属性。
通过initialProperties,这个RCTRootView的初始化函数的参数来完成。
RCTRootView还有一个appProperties属性,修改这个属性,JS端会调用相应的渲染方法。

我们使用RCTRootView将React Natvie视图封装到原生组件中。RCTRootView是一个UIView容器,承载着React Native应用。同时它也提供了一个联通原生端和被托管端的接口。

1. 从原生组件传递属性到React Native(原生->rn)

通过RCTRootView的初始化函数你可以将任意属性传递给React Native应用。参数initialProperties必须是NSDictionary的一个实例。这一字典参数会在内部被转化为一个可供JS组件调用的JSON对象。
原生oc代码:

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];

js处理代码:

'use strict';
import React, { Component } from 'react';
import {
  AppRegistry,
  View,
  Image,
} from 'react-native';

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

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

2. 从原生组件更新属性到React Native(原生->rn)

RCTRootView同样提供了一个可读写的属性appProperties。在appProperties设置之后,React Native应用将会根据新的属性重新渲染。当然,只有在新属性和之前的属性有区别时更新才会被触发。

NSArray *imageList = @[@"http://foo.com/bar3.png",
                   @"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};

你可以随时更新属性,但是更新必须在主线程中进行,读取则可以在任何线程中进行。
更新属性时并不能做到只更新一部分属性。我们建议你自己封装一个函数来构造属性。
注意:目前,最顶层的RN组件(即registerComponent方法中调用的那个)的componentWillReceiveProps和componentWillUpdateProps方法在属性更新后不会触发。但是,你可以通过componentWillMount访问新的属性值。

3. 从React Native传递属性到原生组件(rn->原生)

在你自定义的原生组件中通过RCT_CUSTOM_VIEW_PROPERTY宏导出属性,就可以直接在React Native中使用,就好像它们是普通的React Native组件一样。
更详细的“原生UI组件封装”部分会讲到。

二、原生模块

原生模块是JS中也可以使用的Objective-C类。一般来说这样的每一个模块的实例都是在每一次通过JS bridge通信时创建的。他们可以导出任意的函数和常量给React Native。相关细节可以参阅这篇文章。

事实上原生模块的单实例模式限制了嵌入。假设我们有一个React Native组件被嵌入了一个原生视图,并且我们希望更新原生的父视图。使用原生模块机制,我们可以导出一个函数,不仅要接收预设参数,还要接收父视图的标识。这个标识将会用来获得父视图的引用以更新父视图。那样的话,我们需要维持模块中标识到原生模块的映射。 虽然这个解决办法很复杂,它仍被用在了管理所有React Native视图的RCTUIManager类中,原生模块同样可以暴露已有的原生库给JS,地理定位库就是一个现成的例子。

警告:所有原生模块共享同一个命名空间。创建新模块时注意命名冲突。

在React Native中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的Objective-C类,其中RCT是ReaCT的缩写。

// CalendarManager.h
#import "RCTBridgeModule.h"

@interface CalendarManager : NSObject <RCTBridgeModule>
@end
为了实现RCTBridgeModule协议,你的类需要包含RCT_EXPORT_MODULE()宏。这个宏也可以添加一个参数用来指定在Javascript中访问这个模块的名字。如果你不指定,默认就会使用这个Objective-C类的名字。

// CalendarManager.m
@implementation CalendarManager

//  必须实现
RCT_EXPORT_MODULE();

@end

必须明确的声明要给Javascript导出的方法,否则React Native不会导出任何方法。

1. 普通调用

OC中声明要给Javascript导出的方法,通过RCT_EXPORT_METHOD()宏来实现:

//  对外提供调用方法(testNormalEvent为方法名,后面为参数,按顺序和对应数据类型在js进行传递)
RCT_EXPORT_METHOD(testNormalEvent:(NSString *)name forSomething:(NSString *)thing){
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@", name, thing];
    NSLog(@"%@", info);
}

现在从Javascript里可以这样调用
首先,先导入和声明原生模块:

//  导入NativeModules
import { NativeModules } from 'react-native';
//  声明CalendarManager
var CalendarManager = NativeModules.CalendarManager;

然后,再进行调用:

//  调用原生方法
CalendarManager.addEvent('调用testNormalEvent方法', '测试普通调用')

**注意: **
导出到Javascript的方法名是Objective-C的方法名的第一个部分。React Native还定义了一个RCT_REMAP_METHOD()宏,它可以指定Javascript方法名。当许多方法的第一部分相同的时候用它来避免在Javascript端的名字冲突。
桥接到Javascript的方法返回值类型必须是void。React Native的桥接操作是异步的,所以要返回结果给Javascript,你必须通过回调或者触发事件来进行。

参数类型

RCT_EXPORT_METHOD 支持所有标准JSON类型,包括:

  • string (NSString)
  • number (NSInteger, float, double, CGFloat, NSNumber)
  • boolean (BOOL, NSNumber)
  • array (NSArray) 包含本列表中任意类型
  • object (NSDictionary) 包含string类型的键和本列表中任意类型的值
  • function (RCTResponseSenderBlock)

除此以外,任何RCTConvert类支持的的类型也都可以使用(参见RCTConvert了解更多信息)。RCTConvert还提供了一系列辅助函数,用来接收一个JSON值并转换到原生Objective-C类型或类。

特殊参数类型处理(Date对象)

在我们的CalendarManager例子里,我们需要把事件的时间交给原生方法。我们不能在桥接通道里传递Date对象,所以需要把日期转化成字符串或数字来传递。我们可以这么实现原生函数:

RCT_EXPORT_METHOD(testDateEventOne:(NSString *)name forSomething:(NSString *)thing data:(NSNumber*)secondsSinceUnixEpoch)
{
  NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
}

或者这样:

RCT_EXPORT_METHOD(testDateEventTwo:(NSString *)name forSomething:(NSString *)thing date:(NSString *)ISO8601DateString)
{
  NSDate *date = [RCTConvert NSDate:ISO8601DateString];
}

不过我们可以依靠自动类型转换的特性,跳过手动的类型转换,而直接这么写:

RCT_EXPORT_METHOD(testDateEvent:(NSString *)name forSomething:(NSString *)thing date:(NSDate *)date)
{
  // Date is ready to use!
}

在Javascript既可以这样:

var date = new Date();

 // 把日期以unix时间戳形式传递
CalendarManager.testDateEvent('调用testDateEvent方法', '测试date格式', date.getTime());

也可以这样:
// 把日期以ISO-8601的字符串形式传递
CalendarManager.testDateEvent('调用testDateEvent方法', '测试date格式', date.toISOString()); 

两个值都会被转换为正确的NSDate类型。但如果提供一个不合法的值,譬如一个Array,则会产生一个“红屏”报错信息。

dictionary参数

随着CalendarManager.addEvent方法变得越来越复杂,参数的个数越来越多,其中有一些可能是可选的参数。在这种情况下我们应该考虑修改我们的API,用一个dictionary来存放所有的事件参数,像这样:

//  对外提供调用方法,为了演示事件传入属性字段
RCT_EXPORT_METHOD(testDictionaryEvent:(NSString *)name details:(NSDictionary *) dictionary)
{
    NSString *location = [RCTConvert NSString:dictionary[@"thing"]];
    NSDate *time = [RCTConvert NSDate:dictionary[@"time"]];
    NSString *description=[RCTConvert NSString:dictionary[@"description"]];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@\nDescription: %@",name,location,time,description];
    NSLog(@"%@", info);
}

然后在JS里这样调用:

CalendarManager.testDictionaryEvent('调用addEventMoreDetails方法', {
              thing:'测试字典(字段)格式',
              time:date.getTime(),
              description:'就是这么简单~'
            })

注意: 关于数组和映射
Objective-C并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望收到一个字符串数组,但如果JavaScript在调用的时候提供了一个混合number和string的数组,你会收到一个NSArray,里面既有NSNumber也有NSString。对于数组来说,RCTConvert提供了一些类型化的集合,譬如NSStringArray或者UIColorArray,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用RCTConvert的辅助方法来检测和转换值的类型。

2. 回调函数

原生模块还支持一种特殊的参数——回调函数。它提供了一个函数来把返回值传回给JavaScript。

//  对外提供调用方法,演示Callback
RCT_EXPORT_METHOD(testCallbackEvent:(RCTResponseSenderBlock)callback)
{
    NSArray *events=@[@"callback ", @"test ", @" array"];
    callback(@[[NSNull null],events]);
}

RCTResponseSenderBlock只接受一个参数——传递给JavaScript回调函数的参数数组。在上面这个例子里我们用Node.js的常用习惯:第一个参数是一个错误对象(没有发生错误的时候为null),而剩下的部分是函数的返回值。

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

原生模块通常只应调用回调函数一次。但是,它可以保存callback并在将来调用。这在封装那些通过“委托函数”来获得返回值的iOS API时最为常见。RCTAlertManager中就属于这种情况。
如果你想传递一个更接近Error类型的对象给Javascript,可以用RCTUtils.h提供的RCTMakeError函数。现在它仅仅是发送了一个和Error结构一样的dictionary给Javascript,但我们考虑在将来版本里让它产生一个真正的Error对象。

3. Promises

(译注:这一部分涉及到较新的js语法和特性,不熟悉的读者建议先阅读ES6的相关书籍和文档。)

原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的async/await语法则效果更佳。如果桥接原生方法的最后两个参数是RCTPromiseResolveBlock和RCTPromiseRejectBlock,则对应的JS方法就会返回一个Promise对象。

我们把上面的代码用promise来代替回调进行重构:

//  对外提供调用方法,演示Promise使用
RCT_REMAP_METHOD(testPromiseEvent,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
    NSArray *events =@[@"Promise ",@"test ",@" array"];
    if (events) {
        resolve(events);
    } else {
        NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
        reject(@"no_events", @"There were no events", error);
    }
}

现在JavaScript端的方法会返回一个Promise。这样你就可以在一个声明了async的异步函数内使用await关键字来调用,并等待其结果返回。(虽然这样写着看起来像同步操作,但实际仍然是异步的,并不会阻塞执行来等待)。

//获取Promise对象处理
async updateEvents(){
    console.log('updateEvents');
    try{
        var events=await CalendarManager.testPromiseEvent();
        this.setState({events});
    }catch(e){
        console.error(e);
    }
}

//  在对应位置调用
this.updateEvents;

4. 多线程

原生模块不应对自己被调用时所处的线程做任何假设。React Native在一个独立的串行GCD队列中调用原生模块的方法,但这属于实现的细节,并且可能会在将来的版本中改变。
通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行。具体来说,如果模块需要调用一些必须在主线程才能使用的API,那应当这样指定:

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

指定的methodQueue会被你模块里的所有方法共享。如果你的方法中“只有一个”是耗时较长的(或者是由于某种原因必须在不同的队列中运行的),你可以在函数体内用dispatch_async方法来在另一个队列执行,而不影响其他方法:

RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 在这里执行长时间的操作
    ...
    // 你可以在任何线程/队列中执行回调函数
    callback(@[...]);
  });
}

注意: 在模块之间共享分发队列
methodQueue方法会在模块被初始化的时候被执行一次,然后会被React Native的桥接机制保存下来,所以你不需要自己保存队列的引用,除非你希望在模块的其它地方使用它。但是,如果你希望在若干个模块中共享同一个队列,则需要自己保存并返回相同的队列实例;仅仅是返回相同名字的队列是不行的。

这里有一点需要注意,若是要对原生的UI进行操作,则必须在主线程中进行,即影响原生UI的方法要调用:
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI in UI thread here
});
也可实现methodQueue方法,将全部方法在主线程中执行(不推荐)。

5. 导出常量

原生模块可以导出一些常量,这些常量在JavaScript端随时都可以访问。用这种方法来传递一些静态数据,可以避免通过bridge进行一次来回交互。

- (NSDictionary *)constantsToExport
{
  return @{ @"firstDayOfTheWeek": @"Monday" };
}

Javascript端可以随时同步地访问这个数据:

console.log(CalendarManager.firstDayOfTheWeek);

但是注意这个常量仅仅在初始化的时候导出了一次,所以即使你在运行期间改变constantToExport返回的值,也不会影响到JavaScript环境下所得到的结果。

6. 枚举常量

用NS_ENUM定义的枚举类型必须要先扩展对应的RCTConvert方法才可以作为函数参数传递。

假设我们要导出如下的NS_ENUM定义:

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

接着你可以这样定义方法并且导出enum值作为常量:

- (NSDictionary *)constantsToExport
{
  return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone),
            @"statusBarAnimationFade" : @(UIStatusBarAnimationFade),
            @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) }
};

RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation
                                completion:(RCTResponseSenderBlock)callback)

你的枚举现在会用上面提供的选择器进行转换(上面的例子中是integerValue),然后再传递给你导出的函数。

7. 给Javascript发送事件

即使没有被JavaScript调用,本地模块也可以给JavaScript发送事件通知。最直接的方式是使用eventDispatcher:

#import "RCTBridge.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synthesize bridge = _bridge;

//  进行设置发送事件通知给JavaScript端
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
    NSString *name = [notification userInfo][@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
                                                 body:@{@"name": name}];
}

@end

在JavaScript中可以这样订阅事件:

import { NativeAppEventEmitter } from 'react-native';

var subscription = NativeAppEventEmitter.addListener(
  'EventReminder',
  (reminder) => console.log(reminder.name)
);
...
// 千万不要忘记忘记取消订阅, 通常在componentWillUnmount函数中实现。
subscription.remove();

8. 从Swift导出

Swift不支持宏,所以从Swift向React Native导出类和函数需要多做一些设置,但是大致与Objective-C是相同的。

假设我们已经有了一个一样的CalendarManager,不过是用Swift实现的类:

// CalendarManager.swift

@objc(CalendarManager)
class CalendarManager: NSObject {

  @objc func addEvent(name: String, location: String, date: NSNumber) -> Void {
    // Date is ready to use!
  }

}

注意: 你必须使用@objc标记来确保类和函数对Objective-C公开。
接着,创建一个私有的实现文件,并将必要的信息注册到React Native中。

// CalendarManagerBridge.m
#import "RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(CalendarManager, NSObject)

RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date)

@end

请注意,一旦你在IOS中混用2种语言, 你还需要一个额外的桥接头文件,称作“bridging header”,用来导出Objective-C文件给Swift。如果你是通过Xcode菜单中的File>New File来创建的Swift文件,Xcode会自动为你创建这个头文件。在这个头文件中,你需要引入RCTBridgeModule.h。

// CalendarManager-Bridging-Header.h
#import "RCTBridgeModule.h"

也可以使用RCT_EXTERN_REMAP_MODULE和RCT_EXTERN_REMAP_METHOD来改变导出模块和方法的JavaScript调用名称。


这一part的编写主要参考自官方中文文档:原生模块,因为觉得官方的讲述不太清楚,所以自己进行了部分修改,结合了江清清的demo,为了更清晰的展示原生模块和RN之间的交互,将RN部分和原生部分在同一页面中展示,demo如下(上部分为RN页面,下部分为原生view):
demo暂时上传至百度云:https://pan.baidu.com/s/1hrMVNta

rn&原生test.gif

去掉原生UI,部分(可运行)代码如下:
原生OC代码:

#import "CalendarManager.h"
#import "RCTConvert.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synthesize bridge=_bridge;

//  默认名称
RCT_EXPORT_MODULE()

/**
//  指定执行模块里的方法所在的队列
- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}
*/

//  在完整demo中才有用到,用于更新原生UI
- (void)showInfo:(NSString *)info
{
    //  更新UI操作在主线程中执行
    dispatch_async(dispatch_get_main_queue(), ^{
        //Update UI in UI thread here
        
        [[NSNotificationCenter defaultCenter] postNotificationName:@"react_native_test" object:nil userInfo:@{@"info": info}];
    });
}

- (void)showDate:(NSDate *)date withName:(NSString *)name forSomething:(NSString *)thing
{
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init] ;
    [formatter setDateFormat:@"yyyy-MM-dd"];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@", name, thing,[formatter stringFromDate:date]];
    NSLog(@"%@", info);
}


//  对外提供调用方法
RCT_EXPORT_METHOD(testNormalEvent:(NSString *)name forSomething:(NSString *)thing)
{
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@", name, thing];
    NSLog(@"%@", info);
}

//  对外提供调用方法,为了演示事件时间格式化 secondsSinceUnixEpoch
RCT_EXPORT_METHOD(testDateEventOne:(NSString *)name forSomething:(NSString *)thing data:(NSNumber*)secondsSinceUnixEpoch)
{
    NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch];
    [self showDate:date withName:name forSomething:thing];
}

//  对外提供调用方法,为了演示事件时间格式化 ISO8601DateString
RCT_EXPORT_METHOD(testDateEventTwo:(NSString *)name forSomething:(NSString *)thing date:(NSString *)ISO8601DateString)
{
    NSDate *date = [RCTConvert NSDate:ISO8601DateString];
    [self showDate:date withName:name forSomething:thing];
}

//  对外提供调用方法,为了演示事件时间格式化 自动类型转换
RCT_EXPORT_METHOD(testDateEvent:(NSString *)name forSomething:(NSString *)thing date:(NSDate *)date)
{
    [self showDate:date withName:name forSomething:thing];
}

//  对外提供调用方法,为了演示事件传入属性字段
RCT_EXPORT_METHOD(testDictionaryEvent:(NSString *)name details:(NSDictionary *) dictionary)
{
    NSString *location = [RCTConvert NSString:dictionary[@"thing"]];
    NSDate *time = [RCTConvert NSDate:dictionary[@"time"]];
    NSString *description=[RCTConvert NSString:dictionary[@"description"]];
    
    NSString *info = [NSString stringWithFormat:@"Test: %@\nFor: %@\nTestTime: %@\nDescription: %@",name,location,time,description];
    NSLog(@"%@", info);
}


//  对外提供调用方法,演示Callback
RCT_EXPORT_METHOD(testCallbackEvent:(RCTResponseSenderBlock)callback)
{
    NSArray *events=@[@"callback ", @"test ", @" array"];
    callback(@[[NSNull null],events]);
}


//  对外提供调用方法,演示Promise使用
RCT_REMAP_METHOD(testPromiseEvent,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
    NSArray *events =@[@"Promise ",@"test ",@" array"];
    if (events) {
        resolve(events);
    } else {
        NSError *error=[NSError errorWithDomain:@"我是Promise回调错误信息..." code:101 userInfo:nil];
        reject(@"no_events", @"There were no events", error);
    }
}


//  对外提供调用方法,演示Thread使用
RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback)
{
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 在后台执行耗时操作
        // You can invoke callback from any thread/queue
        callback(@[[NSNull null],@"耗时操作执行完成..."]);
    });
}


//  进行设置封装常量给JavaScript进行调用
-(NSDictionary *)constantsToExport
{
    // 此处定义的常量为js订阅原生通知的通知名
    return @{@"receiveNotificationName":@"receive_notification_test"};
}



//  开始订阅通知事件
RCT_EXPORT_METHOD(startReceiveNotification:(NSString *)name)
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(calendarEventReminderReceived:)
                                                 name:name
                                               object:nil];
}

//  进行设置发送事件通知给JavaScript端(这里需要注意,差一个触发通知点,需自己想办法加上,或者看完整demo)
- (void)calendarEventReminderReceived:(NSNotification *)notification
{
    NSString *name = [notification userInfo][@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder"
                                                 body:@{@"name": name}];
}

@end

js代码:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */
import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  ScrollView,
  TouchableHighlight
} from 'react-native';

import { 
  NativeModules,
  NativeAppEventEmitter 
} from 'react-native';

var subscription;
var CalendarManager = NativeModules.CalendarManager;

class CustomButton extends React.Component {
  render() {
    return (
      <TouchableHighlight
        style={{padding: 8, backgroundColor:this.props.backgroundColor}}
        underlayColor="#a5a5a5"
        onPress={this.props.onPress}>
        <Text>{this.props.text}</Text>
      </TouchableHighlight>
    );
  }
}

class ModulesDemo extends Component {

  constructor(props){
    super(props);
    this.state={
        events:'',
        notice:'',
    }
  }

  componentDidMount(){
    console.log('开始订阅通知...');
    this.receiveNotification();

    subscription = NativeAppEventEmitter.addListener(
         'EventReminder',
          (reminder) => {
            console.log('通知信息:'+reminder.name)
            this.setState({notice:reminder.name});
          }
         );
  }

  receiveNotification(){
    //  CalendarManager.receiveNotificationName 为原生定义常量
    CalendarManager.startReceiveNotification(CalendarManager.receiveNotificationName);
  }

  componentWillUnmount(){
     subscription.remove();
  }

  //获取Promise对象处理
  async updateEvents(){
    console.log('updateEvents');
    try{
        var events=await CalendarManager.testPromiseEvent();
        this.setState({events});
    }catch(e){
        console.error(e);
    }
  }

  render() {

    var date = new Date();

    return (
      <ScrollView>
      <View>
        <Text style={{fontSize: 16, textAlign: 'center', margin: 10}}>
          RN模块
        </Text>

        <View style={{borderWidth: 1,borderColor: '#000000'}}>
        <Text style={{fontSize: 15, margin: 10}}>
          普通调用原生模块方法
        </Text>

        <CustomButton text="调用testNormalEvent方法-普通"
            backgroundColor= "#FF0000"
            onPress={()=>CalendarManager.testNormalEvent('调用testNormalEvent方法', '测试普通调用')}
        />

        <CustomButton text="调用testDateEvent方法-日期处理"
            backgroundColor= "#FF7F00"
            onPress={()=>CalendarManager.testDateEvent('调用testDateEvent方法', '测试date格式',date.getTime())}
        />
        <CustomButton text="调用testDictionaryEvent方法-字典"
            backgroundColor= "#FFFF00"
            onPress={()=>CalendarManager.testDictionaryEvent('调用testDictionaryEvent方法', {
              thing:'测试字典(字段)格式',
              time:date.getTime(),
              description:'就是这么简单~'
            })}
        />
        </View>

        <View>
        <Text style={{fontSize: 15, margin: 10}}>
          'Callback返回数据:{this.state.events}
        </Text>

        <CustomButton text="调用原生模块testCallbackEvent方法-Callback"
            backgroundColor= "#00FF00"
            onPress={()=>CalendarManager.testCallbackEvent((error,events)=>{
                if(error){
                  console.error(error);
                }else{
                  this.setState({events:events,});
                }
              }
            )}
        />
        <CustomButton text="调用原生模块testPromiseEvent方法-Promise"
            backgroundColor= "#00FFFF"
            onPress={()=>this.updateEvents()}
        />
        </View>
        
        <View style={{borderWidth: 1,borderColor: '#000000'}}>
        <Text style={{fontSize: 15, margin: 10}}>
          原生调js,接收信息:{this.state.notice}
        </Text>
        </View>

      </View>
      </ScrollView>
    );
  }
}

AppRegistry.registerComponent('NativeTest', () => ModulesDemo);

三、原生UI组件封装

在如今的App中,已经有成千上万的原生UI部件了——其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native已经封装了大部分最常见的组件,譬如ScrollView和TextInput,但不可能封装全部组件。而且,说不定你曾经为自己以前的App还封装过一些组件,React Native肯定没法包含它们。幸运的是,在React Naitve应用程序中封装和植入已有的组件非常简单。

简单的说,就是React Native自身的组件肯定不够我们用,我们需要学会把原生的控件进行封装,以供RN模块使用。

看着这篇东西的字数越来越多,觉得读者应该很难读的下篇幅如此长的文章,一口气吃不了那么多,本人其实也写得晕晕的(望体谅),所以这一部分就暂时放着,这部分更详细的了解可以先参考以下两篇文章:
中文官方:原生UI组件
React Native 原生UI组件--简书


ps:React Native 与原生之间的通信其实也需要去了解下原理,下面推荐几篇关于实现原理的文章:
浅析ReactNative之通信机制(一)
bang's blog : React Native通信机制详解

因为没继续这方面的工作所以好久没更新了,可能代码因为rn的更新会有些问题,最好更新下pod的版本,看看官方文档,看到评论里有相应的讨论,出现问题的朋友最好也看看评论哈哈,可能有解决办法♪───O(≧∇≦)O────♪

已有的成果如下:
1) React Native 简介与入门
2) React Native 环境搭建和创建项目(Mac)
3) React Native 开发之IDE
4) React Native 入门项目与解析
5) React Native 相关JS和React基础

6) React Native 组件生命周期(ES6)
7) React Native 集成到原生项目(iOS)
8) React Native 与原生之间的通信(iOS)
9) React Native 封装原生UI组件(iOS)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容