前言
RN和iOS混合開發(fā)的幾種場景。
- 原生項目中,調(diào)用部分RN頁面。
- 原生頁面中,調(diào)用部分RN組件。
- RN項目中,調(diào)用部分原生頁面。
- RN頁面中,調(diào)用部分原生View。
- RN項目中,調(diào)用部分原生模塊。
場景一和場景二其實是一樣的,因為在RN看來,頁面和組件在廣義上都是組件,對應于原生里的View。
場景三和場景四是一樣的,因為無論RN要調(diào)用原生的頁面還是View,我們最終都是把原生的View交給它調(diào)用。還是那句話,RN那邊的組件對應原生里的View,而沒法對應ViewController。
場景五和場景三、場景四的區(qū)別在于,RN調(diào)用原生頁面或View是指調(diào)用原生視圖層面的東西來做UI布局的(當然這些視圖也可能會有操作事件),而RN調(diào)用原生模塊是指調(diào)用原生功能層面的東西來實現(xiàn)某個功能(例如調(diào)用日歷、通訊錄等模塊,調(diào)用分享、三方登錄、支付等三方SDK,調(diào)用我們自己的某些功能代碼塊,等等)。
上一篇講解了原生調(diào)用RN頁面或組件(場景一和場景二)的詳細開發(fā)步驟,這一篇我們講解RN調(diào)用原生頁面或View(場景三和場景四)的詳細開發(fā)步驟,下一篇講解RN調(diào)用原生模塊(場景五)的詳細開發(fā)步驟。
示例:RN調(diào)用原生頁面或View(場景三和場景四)的詳細開發(fā)步驟
該示例實現(xiàn)的是:在RN頁面里調(diào)用原生的MapView來展示和使用。
其實很簡單的,無非就是為每個原生View都創(chuàng)建一個對應的、繼承自RCTViewManager的子Manager來管理原生View,RN那邊再創(chuàng)建相應的組件來接收一下這個導出的原生View就可以使用了。原生View和子Manager是一一對應的,我們會在子Manager里創(chuàng)建原生View、導出原生View、導出原生View的一些屬性、導出原生View的一些事件來供RN調(diào)用這個原生View。
第一步:創(chuàng)建RN項目,打開對應的原生項目,直接編寫原生部分的代碼
既然是RN調(diào)用原生,這就表示RN項目是占主導地位的,因此我們就不需要額外地創(chuàng)建原生項目,再和RN項目建立連接供它調(diào)用了。而是直接打開RN項目對應的原生項目來編寫原生部分的代碼。
我們這里創(chuàng)建一個RN項目,名字叫作 HybridApp,然后打開它對應的iOS項目,直接編寫原生部分的代碼。

第二步:為MapView創(chuàng)建對應的子Manager來創(chuàng)建它、導出它、管理它
這里,我們?yōu)镸apView創(chuàng)建一個對應的、繼承自RCTViewManager的子Manager,名字叫作INEMapViewManager,來管理MapView。
// INEMapViewManager.h
#import <React/RCTViewManager.h>
#import <MapKit/MapKit.h>
@interface INEMapViewManager : RCTViewManager
@end
// INEMapViewManager.m
#import "INEMapViewManager.h"
@implementation INEMapViewManager
// 導出該原生View
RCT_EXPORT_MODULE(INEMapView)
// 創(chuàng)建并返回該原生View
- (UIView *)view
{
MKMapView *mapView = [[MKMapView alloc] init];
return mapView;
}
@end
上面有三個需要注意的地方:
- 我們的類名使用了
INE前綴以避免與其它框架產(chǎn)生命名沖突。蘋果自有框架使用了兩個字符的前綴,而RN則使用RCT作為前綴。為避免命名沖突,我們建議您在自己的類中使用RNT以外的其它三字符前綴。 -
在使用
RCT_EXPORT_MODULE()宏導出原生View的地方,名字取為類名除去Manager,比如這里的類名為INEMapViewManager,而導出的原生View則取名為INEMapView,到時候RN使用時會自動在后面添加Manager。 - 請不要在
-view方法中給UIView實例設置frame或是backgroundColor屬性,我們的選擇是統(tǒng)一在RN項目里為組件添加這些布局屬性。
第三步:在RN項目中調(diào)用該原生View
其實經(jīng)過第二步,就這么簡單,RN項目中已經(jīng)可以調(diào)用該原生View了,我們來編寫一些RN代碼來看看效果。
// MapView.js
import {requireNativeComponent} from 'react-native';
// requireNativeComponent會自動把'INEMapView'解析為'INEMapViewManager'
export default requireNativeComponent('INEMapView');
// App.js
import React, {Component} from 'react';
import MapView from './js/MapView.js';
export default class App extends Component {
render() {
return (
<MapView style={{flex: 1}}/>
);
}
}
用Xcode運行一下項目,發(fā)現(xiàn)已經(jīng)成功在RN項目里調(diào)用了原生的MapView,諸如捏放和其它的手勢都已經(jīng)完整支持。但是現(xiàn)在我們還不能真正得在RN項目控制它。

第四步:導出原生View的一些屬性,供RN使用
舉例來說,我們希望在RN項目中能夠禁用地圖的手指捏放操作。禁用捏放操作只需要一個布爾值類型的屬性就行了,所以我們添加這么一行:
// INEMapViewManager.m
#pragma mark - 導出原生View的一些屬性,供RN使用
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
導出的這些屬性(如zoomEnabled)都是iOS里原生View的同名屬性,亂改了可用不了的哦,類型也是同樣的道理。
現(xiàn)在要想禁用捏放操作,我們只需要在RN項目設置對應的屬性:
// App.js
<MapView
style={{flex: 1}}
zoomEnabled={false}
/>
但這樣并不能很好的說明這個組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到OC的代碼。要解決這個問題,我們可以創(chuàng)建改造一下MapView.js,讓它通過PropTypes來說明這個組件都有哪些接口可用。
// MapView.js
import React, {Component} from 'react';
import {requireNativeComponent} from 'react-native';
import PropTypes from 'prop-types';
// 這里我們把requireNativeComponent的第二個參數(shù)從null變成了MapView,
// 就使得RN的底層框架可以檢查該MapView的屬性和原生View的是否一致,來減少出現(xiàn)問題的可能。
const INEMapView = requireNativeComponent('INEMapView', MapView);
export default class MapView extends Component {
static propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: PropTypes.bool,
};
render() {
return (
<INEMapView
// 這個代表接收所有原生View導出過來的原生屬性
{...this.props}
/>
);
}
}
這樣RN里的組件使用起來就更清晰了,現(xiàn)在用Xcode運行一下項目,就發(fā)現(xiàn)我們已經(jīng)可以成功地禁用捏放操作了。
第五步:導出原生View的一些事件,供RN使用
現(xiàn)在在RN項目里,我們已經(jīng)有了一個MapView,而且可以通過屬性來控制它,不過我們怎么才能想iOS里那樣來監(jiān)聽并處理用戶對地圖一些操作的事件呢,譬如拖動地圖操作?
我們知道iOS里針對這些事件,都是有相關的代理方法來供我們監(jiān)聽并做處理的,而且上面我們已經(jīng)成功地導出了原生View的屬性,那我們就想到是不是可以為原生View添加一些block作為屬性導出出去,并在這些代理方法里調(diào)用它們,這樣RN那邊就可以使用這些block作為回調(diào)監(jiān)聽我們對MapView的事件操作了。
沒問題,這個思路很穩(wěn),可以實踐。但是眼下有一個問題,我們導出的是系統(tǒng)的MKMapView啊,我們沒法直接為它添加屬性的,那我們就考慮使用Category或者繼承來為系統(tǒng)的類添加屬性了,這里我們就采用繼承吧。于是我們自定義了一個繼承自MKMapView的類,名字叫作INEMapView。
// INEMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface INEMapView : MKMapView
// 添加一個block,用來作為事件監(jiān)聽的回調(diào)
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
// INEMapView.m
#import "INEMapView.h"
@implementation INEMapView
@end
然后修改修改INEMapViewManager。
// INEMapViewManager.h
#import <React/RCTViewManager.h>
#import <MapKit/MapKit.h>
@interface INEMapViewManager : RCTViewManager <MKMapViewDelegate>
@end
// INEMapViewManager.m
#import "INEMapViewManager.h"
#import "INEMapView.h"
@implementation INEMapViewManager
// 導出該原生View
RCT_EXPORT_MODULE(INEMapView)
// 創(chuàng)建并返回該原生View
- (UIView *)view
{
INEMapView *mapView = [INEMapView new];
mapView.delegate = self;
return mapView;
}
#pragma mark - 導出原生View的一些屬性,供RN使用
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)
#pragma mark - MKMapViewDelegate
- (void)mapView:(INEMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (mapView.onRegionChange) {
MKCoordinateRegion region = mapView.region;
mapView.onRegionChange(@{
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@"latitudeDelta": @(region.span.latitudeDelta),
@"longitudeDelta": @(region.span.longitudeDelta),
}
});
}
}
@end
但是在原生端看起來我們是給onRegionChange這個block傳遞了一個region對象,但實際傳遞過去的是一個事件對象event,而真正的region對象被包裹在event.nativeEvent對象里,所以為了在RN端使用起onRegionChange方法來更清晰,我們會在MapView對這個參數(shù)做做處理:
// MapView.js
import React, {Component} from 'react';
import {requireNativeComponent} from 'react-native';
import PropTypes from 'prop-types';
// 這里我們把requireNativeComponent的第二個參數(shù)從null變成了MapView,
// 就使得RN的底層框架可以檢查該MapView的屬性和原生View的是否一致,來減少出現(xiàn)問題的可能。
const INEMapView = requireNativeComponent('INEMapView', MapView);
export default class MapView extends Component {
static propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: PropTypes.bool,
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: PropTypes.func,
};
render() {
return (
<INEMapView
// 這個代表接收所有原生View導出過來的原生屬性
{...this.props}
// 這個代表接收我們自己為原生View擴展的屬性
onRegionChange={(event) => {
if (this.props.onRegionChange) {
// 傳遞出去nativeEvent,這樣外界在使用onRegionChange方法時就可以直接點出他們在原生部分傳過來的參數(shù)了
this.props.onRegionChange(event.nativeEvent);
}
}}
/>
);
}
}
// App.js
import React, {Component} from 'react';
import MapView from './js/MapView.js';
export default class App extends Component {
render() {
return (
<MapView
style={{flex: 1}}
zoomEnabled={false}
onRegionChange={(nativeEvent) => {
console.log(nativeEvent.region);
}}
/>
);
}
}
好,經(jīng)過這一堆操作就可以成功地導出原生View的一些事件,供RN使用了,快去運行試試吧。
第六步:導出原生界面
經(jīng)過以上步驟,我們就成功地實現(xiàn)了RN調(diào)用原生組件,但其實RN調(diào)用原生頁面和這個是一模一樣的操作,只不過我們不能直接導出ViewController,而是導出ViewController.view供RN使用,也就是說RN只能使用原生頁面上頁面的部分,需要自己添加導航欄啊、TabBar啊什么的,如此而已。
- (UIView *)view
{
ViewController *vc = [ViewController new];
return vc.view;
}