Native UI Components
Native UI Components
需要本机代码的项目
此页面仅适用于react-native init
使用Create React Native App 制作的或使用此类应用程序弹出的项目。有关弹出的更多信息,请参阅创建React Native App存储库的指南。
有许多原生UI小部件可以在最新的应用程序中使用 - 其中一些是该平台的一部分,另一些则作为第三方库提供,并且还可能在您自己的产品组合中使用。反应本土有几个最关键的平台组件已经包裹着,像ScrollView
和TextInput
,但不是所有的人,肯定不是你可能会自己写的前一个应用程序的。幸运的是,将这些现有组件与您的React Native应用程序无缝集成非常简单。
就像本地模块指南一样,这也是一个更高级的指南,假设你对iOS编程有些熟悉。本指南将向您展示如何构建本地UI组件,引导您完成MapView
核心React Native库中现有组件的子集的实现。
iOS MapView示例
比方说,我们希望为我们的应用添加一个交互式地图 - 可以使用MKMapView
,我们只需要使其可用于JavaScript。
原生视图由子类创建和操作RCTViewManager
。这些子类在功能上与视图控制器类似,但基本上是单例 - 每个桥只创建一个实例。它们向本地视图公开本地视图,根据需要将视图RCTUIManager
委托给它们以设置和更新视图的属性。该RCTViewManager
s为通常还对意见的代表,通过桥事件发送回JavaScript的。
公开视图很简单:
RCTViewManager
创建组件管理器的子类。
- 添加
RCT_EXPORT_MODULE()
标记宏。
- 实施该
-(UIView *)view
方法。
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
注意:
不要尝试在通过方法公开的实例上设置frame
或backgroundColor
属性。React Native会覆盖您的自定义类设置的值,以匹配您的JavaScript组件的布局道具。如果你需要这个粒度的控制,可能会更好地包装你想在另一个样式中的实例,而不是返回包装。有关更多上下文,请参阅第2948期。UIView-viewUIViewUIViewUIView
在上面的例子中,我们在前面加上了我们的类名
RNT
。前缀用于避免与其他框架的名称冲突。Apple框架使用双字母前缀,React NativeRCT
用作前缀。为了避免名称冲突,我们建议使用除RCT
您自己的类以外的三个字母的前缀。
那么你只需要一点JavaScript就可以使它成为一个可用的React组件:
// MapView.js
import { requireNativeComponent } from 'react-native';
// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap', null
// MyApp.js
import MapView from './MapView.js';
...
render() {
return <MapView style={{ flex: 1 }} />;
}
请务必在RNTMap
这里使用。我们希望在这里要求经理,这将公开我们的经理在Javascript中使用的视图。
注意:
渲染时,不要忘记拉伸视图,否则你会盯着空白屏幕。
render() {
return <MapView style={{flex: 1}} />;
}
现在,这是一个全功能的JavaScript本地地图视图组件,通过捏缩放和其他本地手势支持完成。我们无法真正从JavaScript控制它,尽管:(
属性
我们可以做的第一件事是让这个组件更加可用,这是为了弥合一些本地属性。假设我们希望能够禁用缩放并指定可见区域。禁用缩放是一个简单的布尔值,所以我们添加这一行:
// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
请注意,我们明确指定类型为BOOL
- React Native RCTConvert
在引擎盖下通过桥接器转换各种不同的数据类型,而错误的值将显示方便的“RedBox”错误,以便您尽快知道存在问题。当事情如此简单时,整个实现将由这个宏来为您处理。
现在为了实际禁用缩放,我们在JS中设置属性:
// MyApp.js
<MapView
zoomEnabled={false}
style={{ flex: 1 }}
/>;
要记录我们的MapView组件的属性(以及它们接受哪些值),我们将添加一个包装组件并用React记录界面PropTypes
:
// MapView.js
import PropTypes from 'prop-types';
import React from 'react';
import { requireNativeComponent } from 'react-native';
class MapView extends React.Component {
render() {
return <RNTMap {...this.props} />;
}
}
MapView.propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: PropTypes.bool,
};
var RNTMap = requireNativeComponent('RNTMap', MapView
module.exports = MapView;
现在我们有一个很好的记录包装组件,很容易处理。请注意,我们改变了第二个参数requireNativeComponent
从null
到新的MapView
包装组件。这允许基础结构验证propTypes与本地道具相匹配,以减少ObjC和JS代码之间不匹配的可能性。
接下来,让我们添加更复杂的region
道具。我们从添加本机代码开始:
// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
好的,这比BOOL
我们以前的简单情况更复杂。现在我们有一个MKCoordinateRegion
需要转换函数的类型,并且我们有自定义代码,这样当我们从JS设置区域时,视图将会动画。在我们提供的函数体内,json
指的是从JS传递的原始值。还有一个view
变量可以让我们访问管理器的视图实例,defaultView
如果JS向我们发送一个空的标记,我们使用该变量将属性重置为默认值。
你可以为你的视图编写任何你想要的转换函数 - 这里是MKCoordinateRegion
通过一个类别的实现RCTConvert
。它使用一个已经存在的ReactNative类别RCTConvert+CoreLocation
:
// RNTMapManager.m
#import "RCTConvert+Mapkit.m"
// RCTConvert+Mapkit.h
#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>
@interface RCTConvert (Mapkit)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;
@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]
};
}
@end
这些转换函数的设计目的是通过显示“RedBox”错误,并在遇到丢失键或其他开发人员错误时返回标准初始化值,从而安全地处理JS可能抛出的任何JSON。
为了完成对region
prop的支持,我们需要将它记录在文件中propTypes
(否则我们会得到一个本地prop没有记录的错误),那么我们可以像设置任何其他prop一样设置它:
// MapView.js
MapView.propTypes = {
/**
* A Boolean value that determines whether the user may use pinch
* gestures to zoom in and out of the map.
*/
zoomEnabled: 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: PropTypes.shape{
/**
* Coordinates for the center of the map.
*/
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
/**
* Distance between the minimum and the maximum latitude/longitude
* to be displayed.
*/
latitudeDelta: PropTypes.number.isRequired,
longitudeDelta: PropTypes.number.isRequired,
}),
};
// MyApp.js
render() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{ flex: 1 }}
/>
}
在这里你可以看到,该区域的形状在JS文档中是明确的 - 理想情况下,我们可以对这些东西进行编码,但这还没有发生。
有时,您的本地组件会拥有一些您不希望它们成为相关React组件的API的一部分的特殊属性。例如,为原始本机事件创建Switch
了一个自定义onChange
处理程序,并公开一个onValueChange
仅使用布尔值而非原始事件调用的处理程序属性。既然你不希望这些本地唯一的属性成为API的一部分,你不想把它们放入propTypes
,但如果你不这样做,你会得到一个错误。解决方案只是将它们添加到nativeOnly
选项中,例如
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
nativeOnly: { onChange: true }
}
活动
所以现在我们有一个本地地图组件,我们可以从JS轻松控制,但我们如何处理来自用户的事件,如捏缩放或平移以更改可见区域?
到目前为止,我们刚刚MKMapView
从经理的-(UIView *)view
方法中返回了一个实例。我们不能添加新的属性,MKMapView
所以我们必须创建一个新的子类MKMapView
,我们用它来查看。然后我们可以onRegionChange
在这个子类上添加一个回调函数:
// RNTMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMapView: MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
// RNTMapView.m
#import "RNTMapView.h"
@implementation RNTMapView
@end
接下来,声明一个事件处理程序属性RNTMapManager
,使其成为它公开的所有视图的委托,并通过从本地视图调用事件处理程序块将事件转发给JS。
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.m"
@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
- (UIView *)view
{
RNTMapView *map = [RNTMapView new];
map.delegate = self;
return map;
}
#pragma mark MKMapViewDelegate
- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
if (!mapView.onRegionChange) {
return;
}
MKCoordinateRegion region = mapView.region;
mapView.onRegionChange(@{
@"region": @{
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
@"latitudeDelta": @(region.span.latitudeDelta),
@"longitudeDelta": @(region.span.longitudeDelta),
}
}
}
@end
在委托方法中-mapView:regionDidChangeAnimated:
,使用区域数据在相应的视图上调用事件处理程序块。调用onRegionChange
事件处理程序块会导致在JavaScript中调用相同的回调支持。这个回调函数与原始事件一起被调用,我们通常在包装器组件中处理这个事件来创建一个更简单的API:
// MapView.js
class MapView extends React.Component {
_onRegionChange = (event) => {
if (!this.props.onRegionChange) {
return;
}
// process raw event...
this.props.onRegionChange(event.nativeEvent
}
render() {
return (
<RNTMap
{...this.props}
onRegionChange={this._onRegionChange}
/>
}
}
MapView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: PropTypes.func,
...
};
// MyApp.js
class MyApp extends React.Component {
onRegionChange(event) {
// Do stuff with event.region.latitude, etc.
}
render() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
onRegionChange={this.onRegionChange}
/>
}
}
样式
由于我们所有的原生反应视图都是子类UIView
,因此大多数样式属性都可以像您期望的那样工作。有些组件需要一个默认的样式,但是,例如UIDatePicker
,这是一个固定的大小。此默认样式对于布局算法按预期工作很重要,但我们也希望能够在使用组件时覆盖默认样式。DatePickerIOS
通过将本地组件包装在具有灵活样式的额外视图中,并在内部本地组件上使用固定样式(使用本机传入的常量生成)执行此操作:
// DatePickerIOS.ios.js
import { UIManager } from 'react-native';
var RCTDatePickerIOSConsts = 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
常数由敛像这样的天然组分的实际帧从天然导出:
// 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),
}
};
}
本指南涵盖了桥接自定义本机组件的许多方面,但您可能需要考虑更多内容,例如用于插入和布置子视图的自定义挂钩。如果你想更深入,请查看一些实现组件的源代码。