ReactNative学习笔记之iOS原生UI的封装

学习ReactNative有一段时间了,也加了好多群,看见群里每天那么多人在那儿讨论和学习ReactNative,感觉ReactNative以后会是一个趋势啊!话不多说,干货开始。

今天主要给大家分享一下iOS的原生UI的封装,鉴于官网上的讲解不太全面,有很多坑,我在学习的过程中也是痛苦不堪,现在把我的经验分享给大家,希望大家可以少遇一写坑,也希望大家多多指正!

概述

首先,我们先来大概了解下原生的OC是如何和JS之间实现桥接和沟通的。

原生的部分分为两种,原生UI组件(View)和原生模块(除View之外的其他的类,例如日历类(NSCalendar))。原生部分主要是通过RCTBridge类以及RCTBridgeModule协议来和JS进行桥接和沟通。

对于原生UI组件来说,系统引入了一个RCTViewManager类来管理原生View。同是为了更好的对View做桥接,为RCTBridge类实现了一个分类:RCTBridge(RCTUIManager)。为了更好的对原生事件做桥接,为RCTBridge类实现了一个分类RCTBridge (RCTEventDispatcher)。

对于原生UI的封装,主要包括三个部分,原生UI、管理者(ViewManager)及JS中对应的模块,原生View的主要作用是提供视图呈现以及属性和方法,管理者的主要作用是将原生View以及其开放的属性和方法提供给JS调用,JS中对应的模块的主要作用是对原生View进行包装和与原生View建立联系和映射。

1、原生UI的提供

在iOS中,如果是原生的View可以直接封装,如果是原生的Controller,需要用一个View包装起来,即:自定义一个View,添加一个属性,强引用Controller,然后把Controller的根View添加到当前View中。例如:

#import "MyView.h"
#import "TestViewController.h"
@interface MyView()

@property (strong, nonatomic) TestViewController *testVC;
@property (weak, nonatomic) UIView *testView;

@end

@implementation MyView

#pragma mark - 懒加载
- (UIView *)testView {
  if (!_testView) {
    TestViewController *testVC = [TestViewController new];
    // 记录Controller
    self. testVC = testVC;
    // 取出控制器的根View
    _ testView = testVC.view;
  }
  return _playV;
}

@end

2、RCTViewManager

每一个原生UI都需要被一个RCTViewManager的子类来创建和管理。在程序运行过程中,RCTViewManager会创建原生UI并把视图提供给RCTUIManager,RCTUIManager则反过来委托RCTViewManager在需要的时候去设置和更新视图的属性。

所以,对于一个manager来说,需要实现的基本功能有:提供自身供JS访问;提供视图;提供属性;提供方法。

@interface RCTViewManager : NSObject <RCTBridgeModule>

/**
The bridge can be used to access both the RCTUIIManager and the RCTEventDispatcher, allowing the manager (or the views that it manages) to manipulate the view hierarchy and send events back to the JS context.
*/
@property (nonatomic, weak) RCTBridge *bridge;

/**
This method instantiates a native view to be managed by the module. Override this to return a custom view instance, which may be preconfigured with default properties, subviews, etc. This method will be called many times, and should return a fresh instance each time. The view module MUST NOT cache the returned view and return the same instance for subsequent calls.
*/
- (UIView *)view;

上图是RCTViewManager的头文件,从中我们可以看出,RCTViewManager有一个bridge的属性,一个返回view的方法,并且遵守了协议RCTBridgeModule。

bridge:这个属性被用来访问RCTUIManager和RCTEvenDispatcher,允许通过manager来操作视图层次和将事件发送到JS。

view:通过重写这个方法,来返回一个初始化好的native view,提供给JS模块调用。

RCTBridgeModule:这个协议提供了注册一个桥接模块所需要的接口。

以下是向JS提供一个可用的原生视图的步骤:

2.1 创建一个RCTViewManager的子类,添加RCT_EXPORT_MODULE()标记宏

RCT_EXPORT_MODULE()是一个宏,在这个宏中实现了RCTBridgeModule协议中的方法,这个协议方法可以使得在JS中访问到当前这个模块。

这个宏也可以添加一个参数用来指定在JS中访问这个模块的名字。如果你不指定,默认就会使用这个OC类的名字。

2.2重写RCTViewManager的-(UIView *)view方法

/// 重写这个方法,返回将要提供给JS使用的视图
- (UIView *)view {
  return [[MyView alloc] initWithFrame: [UIScreen mainScreen].bounds];
}

2.3 提供属性

我们通过RCT_EXPORT_VIEW_PROPERTY()这个宏来向JS提供属性。例如:

我们自定义的视图(MyView)中有isChangeBackground属性。

@interface MyView : UIView

@property (assign, nonatomic) BOOL isChangeBackground;

@end

在manager中与之对应的为:

@implementation RCTMyViewManager

RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(isChangeBackground, BOOL)

@end

可以看出,第一次参数为属性名称,第二个参数为数据类型。

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

* string (NSString)
* number (NSInteger, float, double,CGFloat, NSNumber)
* boolean (Bool, NSNumber)
* array (NSArray)数组元素类型包含本列表中任意类型
* object (NSDictionary) 字典元素包含string类型的键和本列表中任意类型的值
* fuction (RCTResponseSenderBlock)

除了以上的列出的常见数据类型,所有RCTConvert类中支持的类型都可以使用,如果使用了自定义的类的数据,RCTConvert还提供了一系列辅助函数,用来接收一个JSON值并转换到原生OC类型或者类。(请参考RCTConvert类)

2.4 提供没有返回值的方法

如果提供给JS的方法没有返回值,我们可以为原生控件添加一个属性来达到调用方法的目的。例如:

自定义视图(MyView)中添加changeValue属性:

@property (assign, nonatomic) double changeValue;

重写changeValue的setter方法:

/// 改变值
- (void)setChangeValue:(double) changeValue {
  NSLog(@"changeValue = %zd", changeValue);
 _changeValue = changeValue;
  
  //执行无返回值的方法
  [self.testVC changeValue:(double) changeValue];
}

可以看出,如果想要改变值,就将想要改变为的值赋值给changeValue属性。当changeValue被赋值后,就会执行改变值的方法。

在manager中与之对应的为:

RCT_EXPORT_VIEW_PROPERTY(changeValue, double)

如果方法有多个参数时,可以添加一个字典属性,字典中存放各个参数。例如:
自定义视图中添加infoDict属性:

// 提供多参数无返回值方法对应的属性(包括参数1(NSString),key为:@“key1”; 参数2(NSInteger),key为:@“key2”;参数3(NSInteger),key为:@“key3”。)
@property (strong, nonatomic) NSDictionary *infoDict;

infoDict有三个指定的键值对,包括:
参数1(NSString),key为:@“key1”;
参数2(NSInteger),key为:@“key2”;
参数3(NSInteger),key为:@“key3”。

重写infoDict的setter方法:

#pragma mark - 提供给JS调用的属性

- (void)setInfoDict:(NSDictionary *)infoDict {
  _ infoDict = infoDict;

  NSLog(@"key1 = %@, key2 = %zd, key3 = %zd", infoDict[@"key1"], [infoDict[@"key2"] integerValue], [infoDict[@"key3"] integerValue]);
  [self.testVC notReturnValueFunctionWithValue1: infoDict[@"key1"] Value2:[infoDict[@"key2"] integerValue] Value3:[infoDict[@"key3"] integerValue]];
}

当infoDict被赋值后,就是没有返回值的方法。参数为字典中指定key所对应的值。
在manager中与之对应的为:

RCT_EXPORT_VIEW_PROPERTY(infoDict, NSDictionary)

2.5 提供有返回值的方法

如果有返回值,我们通过RCT_EXPORT_METHOD()这个宏来导入方法给JS调用。例如:获取当前播放时间的方法。

在自定义视图(MyView)中:

.h文件中声明方法。

/// 获取当前数量
- (double) testFunction;

.m文件中实现方法。

/// 演示方法
- (double) testFunction {
  return 1;
}

接下来,我们只要在manager提供给JS的方法中拿到当前view,然后执行view的对象方法,获取到的返回值通过manager返回给JS即可。

在manager中如何拿到当前view呢?

首先要知道一点,并不是说MyView的RCTMyViewmanager,在JS中就能拿到MyView,由于会经过中间桥接文件的转换和JS中通过的DOM树形结构管理视图,所以在JS中拿到MyView,需要通过MyView的tag才能拿到。所以我们想要在JS中执行MyView提供的方法,首先要拿到这个MyView。如何通过tag拿到MyView呢?

前边提到原生模块都是通过RCTBridge和JS进行沟通,而原生View的RCTViewManager又是通过RCTUIManager在JS中展示View,那么他们之间的关系是什么呢?

@interface RCTViewManager : NSObject <RCTBridgeModule>
@property (nonatomic, weak) RCTBridge *bridge;

@implementation RCTBridge (RCTUIManager)
- (RCTUIManager *)uiManager
{
return [self moduleForClass:[RCTUIManager class]];
}
@end

可以看到,RCTViewManager都包含一个RCTBridge的属性,而官方为RCTBridge提供了一个RCTUIManager的分类,用来提供View的桥接。
所以,他们的关系是,view通过RCTViewManager来管理,RCTViewManager通过经过扩展后的RCTBridge来和JS进行桥接,扩展后的RCTBridge通过其中的RCTUIManager提供专门针对View的桥接。
而在RCTUIManager中,提供了如下的方法:

/**
Gets the view associated with a reactTag.
*/
- (UIView *)viewForReactTag:(NSNumber *)reactTag;

所以在manager中通过tag拿到view的方法应该是这样的:

/// 拿到当前View
- (MyView *) getViewWithTag:(NSNumber *)tag {
  NSLog(@"%@", [NSThread currentThread]);
    
  UIView *view = [self.bridge.uiManager viewForReactTag:tag];
  return [view isKindOfClass:[MyView class]] ? (MyView *)view : nil;
}

其中参数tag是从JS中传入的参数。

注意:我在方法中打印了当前线程,为什么要这么做呢?因为实际应用中仅仅这样写会报一个错误。

我们看官方提供的-(UIView *)viewForReactTag:(NSNumber *)reactTag方法的具体实现:

- (UIView *)viewForReactTag:(NSNumber *)reactTag{
RCTAssertMainThread();
return _viewRegistry[reactTag];
}

方法的实现中有一个关于主线程断言的宏,OC中一般用到断言的地方,说明需要提醒使用者,当前方法调用时需要满足一些特定条件。我们再来看断言的内容:

/**
Convenience macro for asserting that we're running on main thread.
*/
#define RCTAssertMainThread() RCTAssert([NSThread isMainThread],
@"This function must be called on the main thread")

可以看到,在这个断言中,提示使用者,这个方法必须在主线程执行。RN中JS与原生通信时都是走的子线程,所以我们需要回到主线程执行上述方法。

RCTViewManager提供了一个函数,用来指定当前manger中的方法在哪个线程执行。实现如下方法,则代表当前manager中的所有方法都会在主线程执行。

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

拿到了view,我们就可以利用view来调用它内部的对象方法了。所以,通过manager提供给JS调用的带返回值的方法的写法如下:

#pragma mark - 导出函数供JS调用

RCT_EXPORT_METHOD(testFunction:(nonnull NSNumber *)reactTag
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject){
  
    NSLog(@"获取当前时间方法被调用了");
    MyView *myView = [self getViewWithTag:reactTag];
    
    NSLog(@"%@", [NSThread currentThread]);
    
    NSNumber *number = [NSNumber numberWithDouble:[myView testFunction]];
    if (number) {
      resolve(number);
    }else {
      reject(@"1002", @"获取数值出错", [NSError errorWithDomain:@"获取数值出错" code:1002 userInfo:nil]);
    }
}

在上边的方法中,testFunction是方法名,在JS中调用就是使用这个名字,参数reactTag是JS中传过来的view的tag,用于拿到当前view;参数reslove和reject都是一个block,分别是成功的回调和失败的回调。

在这个方法中,当获取number成功时,把number当做reslove的参数传入(reslove的参数是id类型,即,必须为对象),在JS中拿到这个block的参数就拿到了当前时间的返回值。

如果方法还带有参数,可在reactTag之后,reslove之前随意添加任意数量的参数即可。

2.6 从原生向JS发送通知

某些情况下自定义的view会接收到通知后执行一些方法,这个时候就需要JS中能够接收到原生的通知。

@implementation RCTBridge (RCTEventDispatcher)
- (RCTEventDispatcher *)eventDispatcher
{
return [self moduleForClass:[RCTEventDispatcher class]];
}
@end

如上图所示,在bredge中,官方同样对RCTBridge进行了RCTEventDispatcher的扩展,用于跨语言发送消息。

manager中的写法如下:

// 注册manager为通知观察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testNotificationEventReminderReceived:) name:@"testNotification" object:nil];


/// 接收到通知后执行的方法
- (void)testNotificationEventReminderReceived:(NSNotification *)notification
{
  NSLog(@"=====================接收到了完成播放的通知");
  NSLog(@"%@", notification);
  NSString *eventName = notification.name;
  NSLog(eventName);
  /// 将消息转发到JS中
  [self.bridge.eventDispatcher sendAppEventWithName:@"testNotification" body:@{@"name": eventName}];
}

可以看到,在manager接受到通知后,通过调用bridge.eventDispatcher的sendAppEventWithName:body:的方法,将通知转发到了JS中。

3、在JS中调用提供的原生UI

3.1 在JS中导出原生UI和使用属性

首先创建原生UI映射到JS中的组件的类文件MyView.js.

在这个文件中导出原生UI并添加导出的属性。

import React, { Component } from 'react';
import { View,requireNativeComponent } from 'react-native';
export default class MyView extends React.Component {    
    // 与OC中 RCTViewManager子类中导出的属性对应    
     static propTypes = {        
             isChangeBackground:           React.PropTypes.bool,
             changeValue:                  React.PropTypes.number,
             infoDict:                     React.PropTypes.object,
     };    
      componentDidMount() {        
             console.log("MyView被加载了");    
    }   
    render() {        
           return(            
                <RCTMyView              
                          {...this.props}
                >           
               </RCTMyView>       
           );    
    }  
}

// 这个文件中,凡是用到RCTMyView的地方,应该与OC中
// RCTViewManager子类中RCT_EXPORT_MODULE()括号中的参数一致,
// 如果没有参数,应为RCTViewManager子类的类名去掉manager
var RCTMyView = requireNativeComponent('RCTMyView', MyView);

3.2 在JS中调用导出的方法;

首先导出我们在OC中写的view的manager,只有通过manager才能调用其中导出的方法。

import React, { Component } from 'react';
import {    
    NativeModules,
} from 'react-native';
const MyViewManager = NativeModules.MyViewManager;

然后这样来调用方法:

MyViewManager.testFunction(findNodeHandle(this.refs.theMyView)).then((r)=>{ 
     console.log('————————————————' + r);
},(e)=>{   
     console.log('————————————————————e');
});

其中,findNodeHandle(this.refs.theMyView)是要求传入的reactTag,.then后边的是成功和失败的回调的实现。其中r为成功回调的参数,e为失败回调的参数。调用方法不一定要写在我们的原生对应的JS中,在任何地方都可以导出View的manager,导出manager的文件中就可以使用manager调用方法。

findNodeHandle(this.refs.theMyView)的使用需要注意一下几点:

首先要导入这个方法,导入方法如下:

import React, { Component } from 'react';
import {    
    findNodeHandle,
} from 'react-native';

其次,这个方法中传入的参数,是你在JS中使用原生view时所指定的ref,其中,this.refs获取到的是当前视图中所有的子视图的ref,theMyView是你为原生View指定的值。例如:

class TestView extends Component {    
     render() {    
          return (      
              <MyView
                   ref="theMyView"
                   style={styles.container}
                  isChangeBackground={true}
                  playDict={{'key1': 'string', 
                             'key2': 0,  
                             'key3': 0}}>
               </MyView>
    );  
}         

此处原生视图的ref= “theMyView”,则获取到它的reactTage为findNodeHandle(this.refs.theMyView)。

3.3 在JS中接收OC发送过来的通知

import React, { Component } from 'react';
import {    
   NativeAppEventEmitter,
} from 'react-native';
var subscription = NativeAppEventEmitter.addListener(
    'testNotification',
    (reminder) => console.log(reminder.name)
);

如上图所示,通过创建一个subscription来接收OC发送过来的消息。

对比OC端的代码:

/// 接收到通知后执行的方法
- (void)testNotificationEventReminderReceived:(NSNotification *)notification
{
  NSLog(@"=====================接收到了完成播放的通知");
  NSLog(@"%@", notification);
  NSString *eventName = notification.name;
  NSLog(eventName);
  /// 将消息转发到JS中
  [self.bridge.eventDispatcher sendAppEventWithName:@"testNotification" 
                                               body:@{@"name": eventName}];
}

可以看出,addListener函数的第一个参数要和OC方法中的name参数相同,第二个函数参数的参数为OC方法中的body。所以OC需要传递给JS的数据通过body来传输。

当OC中manager收到通知后,就会执行subscription中的第二个函数参数。

同OC中的通知一样,JS中的subscription使用完毕后也要进行释放,同样一般写在视图被释放的时候。

componentWillUnmount() {    
   subscription.remove();
}

总结:原生UI封装完整的实际流程

第一步:正常书写原生View;

第二步:创建原生View的manager,继承自RCTViewManager;

第三步:在manager中重写-(UIView *)view方法,并实现宏RCT_EXPORT_MODULE()来导出;

第四步:创建对应的JS文件,导出对应的JS模块,此时原生View已经可以在JS中展现出来;

第五步:在原生View中添加要导出的属性和没有返回值的方法对应的属性,在manager中通过宏RCT_EXPORT_VIEW_PROPERTY()导出这些属性,在JS中通过实现staticpropTypes来链接原生属性;

第六步:导出原生View提供的带有返回值的方法,在manager中通过宏RCT_EXPORT_METHOD()导出这些方法,在JS中导出manager,利用manager调用这些方法;

第七步:原生调用JS的函数,当manager收到通知后,通过桥接属性调用方法sendAppEventWithName:body:向JS中发送消息,在JS中通过创建一个subscription变量来接收消息,记得释放subscription哦;

第八步:快乐的使用提供的原生UI吧!

示例demo:

https://github.com/zhangxiaoshan618/ReactNative_MyViewController.git

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

推荐阅读更多精彩内容