原生開發,發展到今天已經非常成熟完善,已有組件成千上萬,極大的提高了開發效率。而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,就沒用到)。
TestScrollView.h
#import "UIView+React.h",對原生視圖進行擴展(這裡有個重要的屬性reactTag,後面會用到,作為區分用途)。#import "SDCycleScrollView.h" #import "RCTComponent.h" #import "UIView+React.h" @interface TestScrollView : SDCycleScrollView @property (nonatomic, copy) RCTBubblingEventBlock onClickBanner; @end
在封裝的UIView中聲明RCTBubblingEventBlock或RCTBubblingEventBlock類型的block屬性,才可以被當做事件導出。(新的事件導出方式,後面會用到哦)
TestScrollView.m
注意:聲明block屬性名稱要以on開頭(不確定為什麼,在不做其它配置的情況下,只有on開頭能成功)二、創建RCTViewManager子類來創建和管理原生視圖#import "TestScrollView.h" @implementation TestScrollView /** * 挺多封裝原生的第三方組件都會這麼寫,這裡還沒研究透徹,就沒按著去實現 - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = bridge.eventDispatcher; _bridge = bridge; ...... } return self; } */ @end
原生視圖都需要被一個RCTViewManager的子類來創建和管理。
這些管理器在功能上有些類似“視圖控制器”,但它們本質上都是單例 - React Native只會為每個管理器創建一個實例。
它們創建原生的視圖並提供給RCTUIManager,RCTUIManager則會反過來委托它們在需要的時候去設置和更新視圖的屬性。RCTViewManager還會代理視圖的所有委托,並給JavaScript發回對應的事件。提供原生視圖步驟如下:
首先創建一個子類—— 命名規范為“視圖名稱+Manager”. 視圖名稱可以加上自己的前綴,這裡最好避免使用RCT前綴,除非你想給官方pull request添加RCT_EXPORT_MODULE()標記宏—— 讓模塊接口暴露給JavaScript實現-(UIView *)view方法—— 創建並返回組件視圖封裝屬性及傳遞事件下面先貼出完整的代碼,然後會對屬性和事件進行進一步的解說。
TestScrollViewManager.hTestScrollViewManager.m#import "RCTViewManager.h" @interface TestScrollViewManager : RCTViewManager @end
屬性#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實現
1) 實現customDirectEventTypes,返回自定義的事件名數組(on開頭才有效)- (NSArray *) customDirectEventTypes { return @[@"onClickBanner"]; }
2) sendInputEventWithName實現事件調用(reactTag用於實例的區分)
[self.bridge.eventDispatcher sendInputEventWithName:@"onClickBanner" body:@{@"target": cycleScrollView.reactTag, @"value": [NSNumber numberWithInteger:index+1] }];
通過RCTBubblingEventBlock實現
1) 在封裝的View中添加RCTBubblingEventBlock的block屬性(on開頭才有效)@property (nonatomic, copy) RCTBubblingEventBlock onClickBanner;
2) 在Manager類中通過宏RCT_EXPORT_VIEW_PROPERTY完成Block屬性的映射和導出
RCT_EXPORT_VIEW_PROPERTY(onClickBanner, RCTBubblingEventBlock)
3) 實現事件調用(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.jsvar TestScrollView = require('./TestScrollView'); // requireNativeComponent 自動把這個組件提供給 "TestScrollView" // 如果不新建TestScrollView.js對原生組件封裝聲明,則直接用這句導入即可 // var TestScrollView = requireNativeComponent('TestScrollView', null); // 導入常量 var TestScrollViewConsts = require('react-native').UIManager.TestScrollView.Constants; var bannerImgs = [ 'http://file.qingyaoweb.com/d/file/shujuku/h2yklxvwil3.png?imageMogr2/auto-orient/strip%7CimageView2/2', 'http://file.qingyaoweb.com/d/file/shujuku/4zlp2ielclx.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/480/q/100', 'http://file.qingyaoweb.com/d/file/shujuku/yq1mvv10jyv.gif' ]; 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才能將字典取出,在具體調用裡面的值。(這裡也還未研究透徹、需要指導)
React Native 與原生代碼之間混合互相調用。經典項目如下:
鏈接: https://pan.baidu.com/s/1kVwRnYB 密碼: vuwz
以上就是React Native 封裝原生UI組件(iOS)的全文介紹,希望對您學習和使用ios應用開發有所幫助.
【React Native 封裝原生UI組件(iOS)】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!