前言
RN和iOS混合開發(fā)的幾種場(chǎng)景。
- 原生項(xiàng)目中巢音,調(diào)用部分RN頁(yè)面遵倦。
- 原生頁(yè)面中,調(diào)用部分RN組件港谊。
- RN項(xiàng)目中骇吭,調(diào)用部分原生頁(yè)面。
- RN頁(yè)面中歧寺,調(diào)用部分原生View燥狰。
- RN項(xiàng)目中棘脐,調(diào)用部分原生模塊。
場(chǎng)景一和場(chǎng)景二其實(shí)是一樣的龙致,因?yàn)樵赗N看來(lái)蛀缝,頁(yè)面和組件在廣義上都是組件,對(duì)應(yīng)于原生里的View目代。
場(chǎng)景三和場(chǎng)景四是一樣的屈梁,因?yàn)闊o(wú)論RN要調(diào)用原生的頁(yè)面還是View,我們最終都是把原生的View交給它調(diào)用榛了。還是那句話在讶,RN那邊的組件對(duì)應(yīng)原生里的View,而沒(méi)法對(duì)應(yīng)ViewController霜大。
場(chǎng)景五和場(chǎng)景三构哺、場(chǎng)景四的區(qū)別在于,RN調(diào)用原生頁(yè)面或View是指調(diào)用原生視圖層面的東西來(lái)做UI布局的(當(dāng)然這些視圖也可能會(huì)有操作事件)战坤,而RN調(diào)用原生模塊是指調(diào)用原生功能層面的東西來(lái)實(shí)現(xiàn)某個(gè)功能(例如調(diào)用日歷曙强、通訊錄等模塊,調(diào)用分享途茫、三方登錄碟嘴、支付等三方SDK,調(diào)用我們自己的某些功能代碼塊囊卜,等等)娜扇。
上一篇講解了原生調(diào)用RN頁(yè)面或組件(場(chǎng)景一和場(chǎng)景二)的詳細(xì)開發(fā)步驟,這一篇我們講解RN調(diào)用原生頁(yè)面或View(場(chǎng)景三和場(chǎng)景四)的詳細(xì)開發(fā)步驟边败,下一篇講解RN調(diào)用原生模塊(場(chǎng)景五)的詳細(xì)開發(fā)步驟袱衷。
示例:RN調(diào)用原生頁(yè)面或View(場(chǎng)景三和場(chǎng)景四)的詳細(xì)開發(fā)步驟
該示例實(shí)現(xiàn)的是:在RN頁(yè)面里調(diào)用原生的MapView來(lái)展示和使用捎废。
其實(shí)很簡(jiǎn)單的笑窜,無(wú)非就是為每個(gè)原生View都創(chuàng)建一個(gè)對(duì)應(yīng)的、繼承自RCTViewManager
的子Manager來(lái)管理原生View登疗,RN那邊再創(chuàng)建相應(yīng)的組件來(lái)接收一下這個(gè)導(dǎo)出的原生View就可以使用了排截。原生View和子Manager是一一對(duì)應(yīng)的,我們會(huì)在子Manager里創(chuàng)建原生View辐益、導(dǎo)出原生View断傲、導(dǎo)出原生View的一些屬性、導(dǎo)出原生View的一些事件來(lái)供RN調(diào)用這個(gè)原生View智政。
第一步:創(chuàng)建RN項(xiàng)目认罩,打開對(duì)應(yīng)的原生項(xiàng)目,直接編寫原生部分的代碼
既然是RN調(diào)用原生续捂,這就表示RN項(xiàng)目是占主導(dǎo)地位的垦垂,因此我們就不需要額外地創(chuàng)建原生項(xiàng)目宦搬,再和RN項(xiàng)目建立連接供它調(diào)用了。而是直接打開RN項(xiàng)目對(duì)應(yīng)的原生項(xiàng)目來(lái)編寫原生部分的代碼劫拗。
我們這里創(chuàng)建一個(gè)RN項(xiàng)目间校,名字叫作 HybridApp
,然后打開它對(duì)應(yīng)的iOS項(xiàng)目页慷,直接編寫原生部分的代碼憔足。
第二步:為MapView創(chuàng)建對(duì)應(yīng)的子Manager來(lái)創(chuàng)建它、導(dǎo)出它酒繁、管理它
這里滓彰,我們?yōu)镸apView創(chuàng)建一個(gè)對(duì)應(yīng)的、繼承自RCTViewManager的子Manager州袒,名字叫作INEMapViewManager
找蜜,來(lái)管理MapView。
// INEMapViewManager.h
#import <React/RCTViewManager.h>
#import <MapKit/MapKit.h>
@interface INEMapViewManager : RCTViewManager
@end
// INEMapViewManager.m
#import "INEMapViewManager.h"
@implementation INEMapViewManager
// 導(dǎo)出該原生View
RCT_EXPORT_MODULE(INEMapView)
// 創(chuàng)建并返回該原生View
- (UIView *)view
{
MKMapView *mapView = [[MKMapView alloc] init];
return mapView;
}
@end
上面有三個(gè)需要注意的地方:
- 我們的類名使用了
INE
前綴以避免與其它框架產(chǎn)生命名沖突稳析。蘋果自有框架使用了兩個(gè)字符的前綴洗做,而RN則使用RCT
作為前綴。為避免命名沖突彰居,我們建議您在自己的類中使用RNT
以外的其它三字符前綴诚纸。 -
在使用
RCT_EXPORT_MODULE()
宏導(dǎo)出原生View的地方,名字取為類名除去Manager
陈惰,比如這里的類名為INEMapViewManager
畦徘,而導(dǎo)出的原生View則取名為INEMapView
,到時(shí)候RN使用時(shí)會(huì)自動(dòng)在后面添加Manager
抬闯。 - 請(qǐng)不要在
-view
方法中給UIView
實(shí)例設(shè)置frame
或是backgroundColor
屬性井辆,我們的選擇是統(tǒng)一在RN項(xiàng)目里為組件添加這些布局屬性。
第三步:在RN項(xiàng)目中調(diào)用該原生View
其實(shí)經(jīng)過(guò)第二步溶握,就這么簡(jiǎn)單杯缺,RN項(xiàng)目中已經(jīng)可以調(diào)用該原生View了,我們來(lái)編寫一些RN代碼來(lái)看看效果睡榆。
// MapView.js
import {requireNativeComponent} from 'react-native';
// requireNativeComponent會(huì)自動(dòng)把'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運(yùn)行一下項(xiàng)目萍肆,發(fā)現(xiàn)已經(jīng)成功在RN項(xiàng)目里調(diào)用了原生的MapView,諸如捏放和其它的手勢(shì)都已經(jīng)完整支持胀屿。但是現(xiàn)在我們還不能真正得在RN項(xiàng)目控制它塘揣。
第四步:導(dǎo)出原生View的一些屬性,供RN使用
舉例來(lái)說(shuō)宿崭,我們希望在RN項(xiàng)目中能夠禁用地圖的手指捏放操作亲铡。禁用捏放操作只需要一個(gè)布爾值類型的屬性就行了,所以我們添加這么一行:
// INEMapViewManager.m
#pragma mark - 導(dǎo)出原生View的一些屬性,供RN使用
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
導(dǎo)出的這些屬性(如zoomEnabled
)都是iOS里原生View的同名屬性奖蔓,亂改了可用不了的哦琅摩,類型也是同樣的道理。
現(xiàn)在要想禁用捏放操作锭硼,我們只需要在RN項(xiàng)目設(shè)置對(duì)應(yīng)的屬性:
// App.js
<MapView
style={{flex: 1}}
zoomEnabled={false}
/>
但這樣并不能很好的說(shuō)明這個(gè)組件的用法——用戶要想知道我們的組件有哪些屬性可以用房资,以及可以取什么樣的值,他不得不一路翻到OC的代碼檀头。要解決這個(gè)問(wèn)題轰异,我們可以創(chuàng)建改造一下MapView.js
,讓它通過(guò)PropTypes
來(lái)說(shuō)明這個(gè)組件都有哪些接口可用暑始。
// MapView.js
import React, {Component} from 'react';
import {requireNativeComponent} from 'react-native';
import PropTypes from 'prop-types';
// 這里我們把requireNativeComponent的第二個(gè)參數(shù)從null變成了MapView搭独,
// 就使得RN的底層框架可以檢查該MapView的屬性和原生View的是否一致,來(lái)減少出現(xiàn)問(wè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
// 這個(gè)代表接收所有原生View導(dǎo)出過(guò)來(lái)的原生屬性
{...this.props}
/>
);
}
}
這樣RN里的組件使用起來(lái)就更清晰了牙肝,現(xiàn)在用Xcode運(yùn)行一下項(xiàng)目,就發(fā)現(xiàn)我們已經(jīng)可以成功地禁用捏放操作了嗤朴。
第五步:導(dǎo)出原生View的一些事件配椭,供RN使用
現(xiàn)在在RN項(xiàng)目里,我們已經(jīng)有了一個(gè)MapView雹姊,而且可以通過(guò)屬性來(lái)控制它股缸,不過(guò)我們?cè)趺床拍芟雐OS里那樣來(lái)監(jiān)聽并處理用戶對(duì)地圖一些操作的事件呢,譬如拖動(dòng)地圖操作吱雏?
我們知道iOS里針對(duì)這些事件敦姻,都是有相關(guān)的代理方法來(lái)供我們監(jiān)聽并做處理的,而且上面我們已經(jīng)成功地導(dǎo)出了原生View的屬性歧杏,那我們就想到是不是可以為原生View添加一些block作為屬性導(dǎo)出出去镰惦,并在這些代理方法里調(diào)用它們,這樣RN那邊就可以使用這些block作為回調(diào)監(jiān)聽我們對(duì)MapView的事件操作了犬绒。
沒(méi)問(wèn)題旺入,這個(gè)思路很穩(wěn),可以實(shí)踐懂更。但是眼下有一個(gè)問(wèn)題眨业,我們導(dǎo)出的是系統(tǒng)的MKMapView
啊,我們沒(méi)法直接為它添加屬性的沮协,那我們就考慮使用Category或者繼承來(lái)為系統(tǒng)的類添加屬性了,這里我們就采用繼承吧卓嫂。于是我們自定義了一個(gè)繼承自MKMapView
的類慷暂,名字叫作INEMapView
。
// INEMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface INEMapView : MKMapView
// 添加一個(gè)block,用來(lái)作為事件監(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
// 導(dǎo)出該原生View
RCT_EXPORT_MODULE(INEMapView)
// 創(chuàng)建并返回該原生View
- (UIView *)view
{
INEMapView *mapView = [INEMapView new];
mapView.delegate = self;
return mapView;
}
#pragma mark - 導(dǎo)出原生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
但是在原生端看起來(lái)我們是給onRegionChange
這個(gè)block傳遞了一個(gè)region
對(duì)象,但實(shí)際傳遞過(guò)去的是一個(gè)事件對(duì)象event
血久,而真正的region
對(duì)象被包裹在event.nativeEvent
對(duì)象里突照,所以為了在RN端使用起onRegionChange
方法來(lái)更清晰,我們會(huì)在MapView
對(duì)這個(gè)參數(shù)做做處理:
// MapView.js
import React, {Component} from 'react';
import {requireNativeComponent} from 'react-native';
import PropTypes from 'prop-types';
// 這里我們把requireNativeComponent的第二個(gè)參數(shù)從null變成了MapView氧吐,
// 就使得RN的底層框架可以檢查該MapView的屬性和原生View的是否一致讹蘑,來(lái)減少出現(xiàn)問(wè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
// 這個(gè)代表接收所有原生View導(dǎo)出過(guò)來(lái)的原生屬性
{...this.props}
// 這個(gè)代表接收我們自己為原生View擴(kuò)展的屬性
onRegionChange={(event) => {
if (this.props.onRegionChange) {
// 傳遞出去nativeEvent筑舅,這樣外界在使用onRegionChange方法時(shí)就可以直接點(diǎn)出他們?cè)谠糠謧鬟^(guò)來(lái)的參數(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)過(guò)這一堆操作就可以成功地導(dǎo)出原生View的一些事件,供RN使用了翠拣,快去運(yùn)行試試吧版仔。
第六步:導(dǎo)出原生界面
經(jīng)過(guò)以上步驟,我們就成功地實(shí)現(xiàn)了RN調(diào)用原生組件误墓,但其實(shí)RN調(diào)用原生頁(yè)面和這個(gè)是一模一樣的操作蛮粮,只不過(guò)我們不能直接導(dǎo)出ViewController
,而是導(dǎo)出ViewController.view
供RN使用谜慌,也就是說(shuō)RN只能使用原生頁(yè)面上頁(yè)面的部分蝉揍,需要自己添加導(dǎo)航欄啊、TabBar啊什么的畦娄,如此而已又沾。
- (UIView *)view
{
ViewController *vc = [ViewController new];
return vc.view;
}