繼上一篇文章的React Native 與原生之間的通信(iOS),我們知道RN與原生通信主要通過屬性、原生模塊、封裝原生UI組件三種方式,上篇文章主要講了前面兩種方式,這篇文章補充下第三種方式。
由于剛入門React Native,知識水平有限,看了官方文檔(一臉懵b),找了好多博客、源碼去研究怎么封裝iOS端的原生控件,結果嘗試了幾天依舊只能對原生UI有簡單的封裝,使得js能調用其屬性以及事件(不完整),所以這篇文章并不完整,希望RN的高手能給些意見或者博客引導,寫得不對的地方歡迎留言和討論。。。
原生開發,發展到今天已經非常成熟完善,已有組件成千上萬,極大的提高了開發效率。而React Native 在Facebook的React.js conf 2015上提出,至今一年多,組件數目肯定沒得和原生的相比。
因此,在使用React Native開發App的過程中,我們可能需要調用RN沒有實現的原生視圖組件或第三方組件。甚至,我們可以把本地模塊構造成一個React Native組件,提供給別人使用。
本文的demo基于SDCycleScrollView,即banner,因為想不到什么好的例子,所以就把在做的項目用到的SDCycleScrollView封裝下,直接給js調用。
SDCycleScrollView為github開源的無限循環自動圖片輪播器。
地址為:https://github.com/gsdios/SDCycleScrollView
里面會用SDWebImage,如果項目已用到SDWebImage,則建議直接把SDCycleScrollView相關代碼拉進項目就OK了。
一、對原生視圖進行進一步封裝
參考其他人對原生視圖的封裝,大多都會新建一個視圖,繼承(或者子視圖包含)原生視圖,里面可能含有事件的調用(這里簡單demo,就沒用到)。
#import "UIView+React.h",對原生視圖進行擴展(這里有個重要的屬性reactTag,后面會用到,作為區分用途)。
TestScrollView.h
#import "SDCycleScrollView.h"
#import "RCTComponent.h"
#import "UIView+React.h"
@interface TestScrollView : SDCycleScrollView
@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;
@end
在封裝的UIView中聲明RCTBubblingEventBlock或RCTBubblingEventBlock類型的block屬性,才可以被當做事件導出。(新的事件導出方式,后面會用到哦)
注意:聲明block屬性名稱要以on開頭(不確定為什么,在不做其它配置的情況下,只有on開頭能成功)
TestScrollView.m
#import "TestScrollView.h"
@implementation TestScrollView
/**
* 挺多封裝原生的第三方組件都會這么寫,這里還沒研究透徹,就沒按著去實現
- (instancetype)initWithBridge:(RCTBridge *)bridge {
if ((self = [super initWithFrame:CGRectZero])) {
_eventDispatcher = bridge.eventDispatcher;
_bridge = bridge;
......
}
return self;
}
*/
@end
二、創建RCTViewManager子類來創建和管理原生視圖
原生視圖都需要被一個RCTViewManager的子類來創建和管理。
這些管理器在功能上有些類似“視圖控制器”,但它們本質上都是單例 - React Native只會為每個管理器創建一個實例。
它們創建原生的視圖并提供給RCTUIManager,RCTUIManager則會反過來委托它們在需要的時候去設置和更新視圖的屬性。RCTViewManager還會代理視圖的所有委托,并給JavaScript發回對應的事件。
提供原生視圖步驟如下:
- 首先創建一個子類 —— 命名規范為“視圖名稱+Manager”. 視圖名稱可以加上自己的前綴,這里最好避免使用RCT前綴,除非你想給官方pull request
- 添加RCT_EXPORT_MODULE()標記宏 —— 讓模塊接口暴露給JavaScript
- *實現-(UIView )view方法 —— 創建并返回組件視圖
- 封裝屬性及傳遞事件
下面先貼出完整的代碼,然后會對屬性和事件進行進一步的解說。
TestScrollViewManager.h
#import "RCTViewManager.h"
@interface TestScrollViewManager : RCTViewManager
@end
TestScrollViewManager.m
#import "TestScrollViewManager.h"
#import "TestScrollView.h" //第三方組件的頭文件
#import "RCTBridge.h" //進行通信的頭文件
#import "RCTEventDispatcher.h" //事件派發,不導入會引起Xcode警告
@interface TestScrollViewManager() <SDCycleScrollViewDelegate>
@end
@implementation TestScrollViewManager
// 標記宏(必要)
RCT_EXPORT_MODULE()
// 事件的導出,onClickBanner對應view中擴展的屬性
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)
// 通過宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導出
RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);
RCT_EXPORT_VIEW_PROPERTY(imageURLStringsGroup, NSArray);
RCT_EXPORT_VIEW_PROPERTY(autoScroll, BOOL);
- (UIView *)view
{
// 實際組件的具體大小位置由js控制
TestScrollView *testScrollView = [TestScrollView cycleScrollViewWithFrame:CGRectZero delegate:self placeholderImage:nil];
// 初始化時將delegate指向了self
testScrollView.pageControlStyle = SDCycleScrollViewPageContolStyleClassic;
testScrollView.pageControlAliment = SDCycleScrollViewPageContolAlimentCenter;
return testScrollView;
}
/**
* 當事件導出用到 sendInputEventWithName 的方式時,會用到
- (NSArray *) customDirectEventTypes {
return @[@"onClickBanner"];
}
*/
#pragma mark SDCycleScrollViewDelegate
/**
* banner點擊
*/
- (void)cycleScrollView:(TestScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index
{
// 這也是導出事件的方式,不過好像是舊方法了,會有警告
// [self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner"
// body:@{@"target": cycleScrollView.reactTag,
// @"value": [NSNumber numberWithInteger:index+1]
// }];
if (!cycleScrollView.onClickBanner) {
return;
}
NSLog(@"oc did click %li", [cycleScrollView.reactTag integerValue]);
// 導出事件
cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]});
}
// 導出枚舉常量,給js定義樣式用
- (NSDictionary *)constantsToExport
{
return @{
@"SDCycleScrollViewPageContolAliment": @{
@"right": @(SDCycleScrollViewPageContolAlimentRight),
@"center": @(SDCycleScrollViewPageContolAlimentCenter)
}
};
}
// 因為這個類繼承RCTViewManager,實現RCTBridgeModule,因此可以使用原生模塊所有特性
// 這個方法暫時沒用到
RCT_EXPORT_METHOD(testResetTime:(RCTResponseSenderBlock)callback) {
callback(@[@(234)]);
}
@end
屬性
RCT_EXPORT_VIEW_PROPERTY(autoScrollTimeInterval, CGFloat);
通過宏RCT_EXPORT_VIEW_PROPERTY完成屬性的映射和導出。
CGFloat為autoScrollTimeInterval的OC數據類型,轉化成js則對應number。
React Native用RCTConvert來在JavaScript和原生代碼之間完成類型轉換。
支持的默認轉換類型(部分)如下:
- string (NSString)
- number (NSInteger, float, double, CGFloat, NSNumber)
- boolean (BOOL, NSNumber)
- array (NSArray) 包含本列表中任意類型
- map (NSDictionary) 包含string類型的鍵和本列表中任意類型的值
如果轉換無法完成,會產生一個“紅屏”的報錯提示,這樣你就能立即知道代碼中出現了問題。如果一切進展順利,上面這個宏就已經包含了導出屬性的全部實現。
ps:更復雜的類型轉換,則涉及到MKCoordinateRegion類型,本文沒做應用,具體可參考官方文檔例子。
事件
js和原生之間需要有事件的交互,例如,在原生實現的代理或者點擊事件,js也需要實時獲取到此類事件時,就需要利用事件進行交互。
事件的實現方式有以下兩種:
- 通過sendInputEventWithName實現
- 實現customDirectEventTypes,返回自定義的事件名數組(on開頭才有效)
- (NSArray *) customDirectEventTypes {
return @[@"onClickBanner"];
}
- sendInputEventWithName實現事件調用(reactTag用于實例的區分)
[self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner"
body:@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]
}];
- 通過RCTBubblingEventBlock實現
- 在封裝的View中添加RCTBubblingEventBlock的block屬性(on開頭才有效)
@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;
- 在Manager類中通過宏RCT_EXPORT_VIEW_PROPERTY完成Block屬性的映射和導出
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)
- 實現事件調用(reactTag用于實例的區分)
cycleScrollView.onClickBanner(@{@"target": cycleScrollView.reactTag,
@"value": [NSNumber numberWithInteger:index+1]});
通過上面兩種方式封裝好的事件,在js中可以直接利用同名函數調用即可(后面會展示)。
不過關于事件這塊,挺多都沒完全弄懂,希望有大神引導引導,比如為什么只能定義on開頭、如何自定義、如何事件數據源的回調等等。。。
樣式
因為我們所有的視圖都是UIView的子類,大部分的樣式屬性應該直接就可以生效。有些屬性定義,需要用到枚舉,則可以利用通過原生傳遞來的常數方式來實現,具體實現如下:
// 導出枚舉常量,給js定義樣式用
- (NSDictionary *)constantsToExport
{
return @{
@"SDCycleScrollViewPageContolAliment": @{
@"right": @(SDCycleScrollViewPageContolAlimentRight),
@"center": @(SDCycleScrollViewPageContolAlimentCenter)
}
};
}
在js中調用則如下:
// 首先獲取到常量
var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants;
// 調用
<TestScrollView style={styles.container}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
/>
ps: 一部分組件會希望使用自己定義的默認樣式,例如UIDatePicker希望自己的大小是固定的。比如大小用原生默認大小,這個例子具體可以參考官方文檔的樣式模塊。
三、在JS中進行調用
在js中調用,可以有兩種方式,一為直接作為擴展React組件調用,二為新建一個組件封裝好,再進行調用。
下文用第二種方式,官方推薦,邏輯比較清晰。
1.先倒入原生組件,新建TestScrollView.js文件,在里面對TestScrollView導入,進行屬性類型聲明等。具體代碼和解釋如下:
TestScrollView.js
// TestScrollView.js
import React, { Component, PropTypes } from 'react';
import { requireNativeComponent } from 'react-native';
// requireNativeComponent 自動把這個組件提供給 "RCTScrollView"
var RCTScrollView = requireNativeComponent('TestScrollView', TestScrollView);
export default class TestScrollView extends Component {
render() {
return <RCTScrollView {...this.props} />;
}
}
TestScrollView.propTypes = {
/**
* 屬性類型,其實不寫也可以,js會自動轉換類型
*/
autoScrollTimeInterval: PropTypes.number,
imageURLStringsGroup: PropTypes.array,
autoScroll: PropTypes.bool,
onClickBanner: PropTypes.func
};
module.exports = TestScrollView;
2.在index.ios.js中進行調用
index.ios.js
var TestScrollView = require('./TestScrollView');
// requireNativeComponent 自動把這個組件提供給 "TestScrollView"
// 如果不新建TestScrollView.js對原生組件封裝聲明,則直接用這句導入即可
// var TestScrollView = requireNativeComponent('TestScrollView', null);
// 導入常量
var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants;
var bannerImgs = [
'http://upload-images.jianshu.io/upload_images/2321678-ba5bf97ec3462662.png?imageMogr2/auto-orient/strip%7CimageView2/2',
'http://upload-images.jianshu.io/upload_images/1487291-2aec9e634117c24b.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/480/q/100',
'http://f.hiphotos.baidu.com/zhidao/pic/item/e7cd7b899e510fb37a4f2df3db33c895d1430c7b.jpg'
];
class NativeUIModule extends Component {
constructor(props){
super(props);
this.state={
bannerNum:0
}
}
render() {
return (
<ScrollView style = {{marginTop:64}}>
<View>
<TestScrollView style={styles.container}
autoScrollTimeInterval = {2}
imageURLStringsGroup = {bannerImgs}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
onClickBanner={(e) => {
console.log('test' + e.nativeEvent.value);
this.setState({bannerNum:e.nativeEvent.value});
}}
/>
<Text style={{fontSize: 15, margin: 10, textAlign:'center'}}>
點擊banner -> {this.state.bannerNum}
</Text>
</View>
</ScrollView>
);
}
}
// 實際組件的具體大小位置由js控制
const styles = StyleSheet.create({
container:{
padding:30,
borderColor:'#e7e7e7',
marginTop:10,
height:200,
},
});
AppRegistry.registerComponent('NativeTest2', () => NativeUIModule);
若使用第一種方式,即使用下面語句進行組件的引用:
var TestScrollView = requireNativeComponent('TestScrollView', null);
則會存在的這樣的問題:
雖然很方便簡單,但這樣并不能很好的說明這個組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到Objective-C的代碼。要解決這個問題,我們可以創建一個封裝組件,并且通過PropTypes來說明這個組件的接口。
注意:我們現在把requireNativeComponent的第二個參數從null變成了用于封裝的組件TestScrollView。這使得React Native的底層框架可以檢查原生屬性和包裝類的屬性是否一致,來減少出現問題的可能。
關于屬性、事件的調用,則是如下直接調用:
<TestScrollView style={styles.container}
autoScrollTimeInterval = {2}
imageURLStringsGroup = {bannerImgs}
pageControlAliment = {TestScrollViewConsts.SDCycleScrollViewPageContolAliment.right}
onClickBanner={(e) => {
console.log('test' + e.nativeEvent.value);
this.setState({bannerNum:e.nativeEvent.value});
}}
/>
關于事件,需要注意的是,事件事件默認傳遞的是字典數據類型,即json,在js中調用需要利用e.nativeEvent才能將字典取出,在具體調用里面的值。(這里也還未研究透徹、需要指導)
四、成果
到這里為止,應該能對原生UI控件進行簡單的封裝和調用了,如果不用到數據源,只是實現代理的第三方控件的話,封裝來讓RN模塊調用是沒用問題的+_+
下面推薦下有用的相關文章:
1.官方文檔——原生UI組件(這是肯定的)
2.React Native構建本地視圖組件
3.React-Native之復用原生UI組件
4.React Native - How to Bridge an Objective-C View Component
還有對第三方對原生tableview的封裝代碼:
https://github.com/aksonov/react-native-tableview
另外,關于交互原理的文章則推薦以下幾篇:
[iOS] 干貨 | 速收藏 | React Native iOS 源碼解析篇 (二)
淺析ReactNative之通信機制(一)
bang's blog : React Native通信機制詳解
demo還是先暫時放到百度云中:
下面是demo的演示效果:
因為沒繼續這方面的工作所以好久沒更新了,可能代碼因為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)
- React Native 封裝原生UI組件(iOS)