有許多 native UI 小部件可以應用到最新的應用程序中——其中一些是平臺的一部分,另外的可以用作第三方庫,并且更多的是它們可以用于你自己的選集中。React Native 有幾個最關鍵的平臺組件已經(jīng)包裝好了,如 ScrollView 和 TextInput,但不是所有的組件都被包裝好了,當然了,你為先前的應用程序寫的組件肯定沒有包裝好。幸運的是,為了與 React Native 應用程序無縫集成,將現(xiàn)存組件包裝起來是非常容易實現(xiàn)的。
正如 native 模塊指南,這也是一種更高級的指南,假定你對 iOS 編程有一定的了解。本指南將向你展示如何構建一個本地的 UI 組件,帶你實現(xiàn)在核心 React Native 庫中可用的現(xiàn)存的 MapView 組件的子集。
如果說我們想在我們的應用程序中添加一個交互式的 Map——不妨用 MKMapView,我們只需要讓它在 JavaScript 中可用。
Native 視圖是通過 RCTViewManager 的子類創(chuàng)建和操做的。這些子類的功能與視圖控制器很相似,但本質上它們是單件模式——橋只為每一個子類創(chuàng)建一個實例。它們將 native 視圖提供給 RCTUIManager,它會傳回到 native 視圖來設置和更新的必要的視圖屬性。RCTViewManager 通常也是視圖的代表,通過橋將事件發(fā)送回 JavaScript。
發(fā)送視圖是很簡單的:
創(chuàng)建基本的子類。
添加標記宏 RCT_EXPORT_MODULE()。
-(UIView *)view 方法。// RCTMapManager.m
#import <MapKit/MapKit.h>
#import "RCTViewManager.h"
@interface RCTMapManager : RCTViewManager
@end
@implementation RCTMapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
然后你需要一些 JavaScript 使之成為有用的 React 組件:
// MapView.js
var { requireNativeComponent } = require('react-native');
module.exports = requireNativeComponent('RCTMap', null);
現(xiàn)在這是 JavaScript 中一個功能完整的 native map 視圖組件了,包括 pinch-zoom 和其他 native 手勢支持。但是我們還不能用 JavaScript 來真正的控制它。
為了使該組件更可用,我們可以做的第一件事是連接一些 native 屬性。比如說我們希望能夠禁用音高控制并指定可見區(qū)域。禁用音高是一個簡單的布爾值,所以我們只添加這一行:
// RCTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
注意我們顯式的指定類型為 BOOL——當談到連接橋時,React Native 使用 hood 下的 RCTConvert 來轉換所有不同的數(shù)據(jù)類型,且錯誤的值會顯示明顯的 “RedBox” 錯誤使你知道這里有 ASAP 問題。當一切進展順利時,這個宏就會為你處理整個實現(xiàn)。
現(xiàn)在要真正的實現(xiàn)禁用音高,我們只需要在 JS 中設置如下所示屬性:
// MyApp.js
<MapView pitchEnabled={false} />
但是這不是很好記錄——為了知道哪些屬性可用以及它們接收了什么值,你的新組件的客戶端需要挖掘 objective-C 代碼。為了更好的實現(xiàn)這一點,讓我們做一個包裝器組件并用 React PropTypes 記錄接口:
// MapView.js
var React = require('react-native');
var { requireNativeComponent } = React;
class MapView extends React.Component {
render() {
return <RCTMap {...this.props} />;
}
}
var RCTMap = requireNativeComponent('RCTMap', MapView);
MapView.propTypes = {
/**
* When this property is set to `true` and a valid camera is associated
* with the map, the camera’s pitch angle is used to tilt the plane
* of the map. When this property is set to `false`, the camera’s pitch
* angle is ignored and the map is always displayed as if the user
* is looking straight down onto it.
*/
pitchEnabled: React.PropTypes.bool,
};
module.exports = MapView;
現(xiàn)在我們有一個很不錯的已記錄的包裝器組件,它使用非常容易。注意我們?yōu)樾碌?MapView 包裝器組件將第二個參數(shù)從 null 改為 requireNativeComponent。這使得基礎設施驗證了 propTypes 匹配native 工具來減少 ObjC 和 JS 代碼之間的不匹配的可能。
接下來,讓我們添加更復雜的 region 工具。從添加 native 代碼入手:
// RCTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
好的,這顯然比之前簡單的 BOOL 情況更加復雜?,F(xiàn)在我們有一個 MKCoordinateRegion 類型,該類型需要一個轉換函數(shù),并且我們有自定義的代碼,這樣當我們從 JS 設置區(qū)域時,視圖可以產生動畫效果。還有一個 defaultView,如果 JS 發(fā)送給我們一個 null 標記,我們使用它將屬性重置回默認值。
當然你可以為你的視圖編寫任何你想要的轉換函數(shù)——下面是通過 RCTConvert 的兩類來實現(xiàn) MKCoordinateRegion 的例子:
@implementation RCTConvert(CoreLocation)
RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue);
RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);
+ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json
{
json = [self NSDictionary:json];
return (CLLocationCoordinate2D){
[self CLLocationDegrees:json[@"latitude"]],
[self CLLocationDegrees:json[@"longitude"]]
};
}
@end
@implementation RCTConvert(MapKit)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
json = [self NSDictionary:json];
return (MKCoordinateSpan){
[self CLLocationDegrees:json[@"latitudeDelta"]],
[self CLLocationDegrees:json[@"longitudeDelta"]]
};
}
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
return (MKCoordinateRegion){
[self CLLocationCoordinate2D:json],
[self MKCoordinateSpan:json]
};
}
這些轉換函數(shù)是為了安全地處理任何 JSON 而設計的,當出現(xiàn)丟失的鍵或開發(fā)人員錯誤操作時,JS 可能向它們拋出 “RedBox” 錯誤并返回標準的初始化值。
為完成對 region 工具的支持,我們需要把它記錄到 propTypes中(否則我們將得到一個錯誤,即 native 工具沒有被記錄),然后我們就可以按照設置其他工具的方式來設置它:
// MapView.js
MapView.propTypes = {
/**
* When this property is set to `true` and a valid camera is associated
* with the map, the camera’s pitch angle is used to tilt the plane
* of the map. When this property is set to `false`, the camera’s pitch
* angle is ignored and the map is always displayed as if the user
* is looking straight down onto it.
*/
pitchEnabled: React.PropTypes.bool,
/**
* The region to be displayed by the map.
*
* The region is defined by the center coordinates and the span of
* coordinates to display.
*/
region: React.PropTypes.shape({
/**
* Coordinates for the center of the map.
*/
latitude: React.PropTypes.number.isRequired,
longitude: React.PropTypes.number.isRequired,
/**
* Distance between the minimum and the maximum latitude/longitude
* to be displayed.
*/
latitudeDelta: React.PropTypes.number.isRequired,
longitudeDelta: React.PropTypes.number.isRequired,
}),
};
// MyApp.js
render() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return <MapView region={region} />;
}
在這里你可以看到該區(qū)域的形狀在 JS 文檔中是顯式的——理想情況下我們可以生成一些這方面的東西,但是這沒有實現(xiàn)。
所以現(xiàn)在我們有一個 native map 組件,可以從 JS 很容易的控制,但是我們如何處理來自用戶的事件,如 pinch-zooms 或平移來改變可見區(qū)域?關鍵是要使 RCTMapManager 成為它發(fā)送的所有視圖的代表,并把事件通過事件調度器發(fā)送給 JS。這看起來如下所示(從整個實現(xiàn)中簡化出來的部分):
// RCTMapManager.m
#import "RCTMapManager.h"
#import <MapKit/MapKit.h>
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import "UIView+React.h"
@interface RCTMapManager() <MKMapViewDelegate>
@end
@implementation RCTMapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
MKMapView *map = [[MKMapView alloc] init];
map.delegate = self;
return map;
}
#pragma mark MKMapViewDelegate
- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated
{
MKCoordinateRegion region = mapView.region;
NSDictionary *event = @{
@"target": mapView.reactTag,
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@"latitudeDelta": @(region.span.latitudeDelta),
@"longitudeDelta": @(region.span.longitudeDelta),
}
};
[self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event];
}
你可以看到我們設置管理器為它發(fā)送的每個視圖的代表,然后在代表方法 -mapView:regionDidChangeAnimated: 中,區(qū)域與 reactTag 目標相結合來產生事件,通過 sendInputEventWithName:body 分派到你應用程序中相應的 React 組件實例中。事件名稱 @"topChange" 映射到從 JavaScript 中回調的 onChange(這里查看 mappings )。原始事件調用這個回調,我們通常在包裝器組件中處理這個過程來實現(xiàn)一個簡單的 API:
// MapView.js
class MapView extends React.Component {
constructor() {
this._onChange = this._onChange.bind(this);
}
_onChange(event: Event) {
if (!this.props.onRegionChange) {
return;
}
this.props.onRegionChange(event.nativeEvent.region);
}
render() {
return <RCTMap {...this.props} onChange={this._onChange} />;
}
}
MapView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: React.PropTypes.func,
...
};
由于我們所有的 native react 視圖是 UIView 的子類,大多數(shù)樣式屬性會像你預想的一樣內存不足。然而,一些組件需要默認的樣式,例如 UIDatePicker,大小固定。為了達到預期的效果,默認樣式對布局算法來說是非常重要的,但是我們也希望在使用組件時能夠覆蓋默認的樣式。DatePickerIOS 通過包裝一個額外的視圖中的 native 組件實現(xiàn)這一功能,該額外的視圖具有靈活的樣式設計,并在內部 native 組件中使用一個固定的樣式(用從 native 傳遞的常量生成):
// DatePickerIOS.ios.js
var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants;
...
render: function() {
return (
<View style={this.props.style}>
<RCTDatePickerIOS
ref={DATEPICKER}
style={styles.rkDatePickerIOS}
...
/>
</View>
);
}
});
var styles = StyleSheet.create({
rkDatePickerIOS: {
height: RCTDatePickerIOSConsts.ComponentHeight,
width: RCTDatePickerIOSConsts.ComponentWidth,
},
});
RCTDatePickerIOSConsts 常量是通過抓取 native 組件的實際框架從 native 中導出的,如下所示:
// RCTDatePickerManager.m
- (NSDictionary *)constantsToExport
{
UIDatePicker *dp = [[UIDatePicker alloc] init];
[dp layoutIfNeeded];
return @{
@"ComponentHeight": @(CGRectGetHeight(dp.frame)),
@"ComponentWidth": @(CGRectGetWidth(dp.frame)),
@"DatePickerModes": @{
@"time": @(UIDatePickerModeTime),
@"date": @(UIDatePickerModeDate),
@"datetime": @(UIDatePickerModeDateAndTime),
}
};
}
本指南涵蓋了銜接自定義 native 組件的許多方面,但有你可能有更多需要考慮的地方,如自定義 hooks 來插入和布局子視圖。如果你想了解更多,請在源代碼中查看實際的 RCTMapManager 和其他組件。