你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> React Native 封裝原生UI組件(iOS)

React Native 封裝原生UI組件(iOS)

編輯:IOS開發綜合

原生開發,發展到今天已經非常成熟完善,已有組件成千上萬,極大的提高了開發效率。而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實現
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.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://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)】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved