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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容