iOS 原生 UI 组件
原生模块和原生组件是我们传统架构中使用的稳定技术。当新架构稳定后,它们将在未来被弃用。新架构使用Turbo 原生模块和Fabric 原生组件来实现类似的结果。
市面上有大量的原生 UI 小部件可用于最新的应用 - 其中一些是平台的一部分,另一些作为第三方库可用,还有一些可能在您自己的作品集中使用。React Native 已经封装了一些最关键的平台组件,例如 ScrollView
和 TextInput
,但并非所有组件,当然也不包括您可能为以前的应用编写的组件。幸运的是,我们可以封装这些现有的组件,以便与您的 React Native 应用程序无缝集成。
与原生模块指南类似,这同样是一个更高级的指南,假设您对 iOS 编程有一定的了解。本指南将向您展示如何构建原生 UI 组件,引导您完成 React Native 核心库中现有 MapView
组件子集的实现。
iOS MapView 示例
假设我们想在我们的应用中添加一个交互式地图 - 最好使用MKMapView
,我们只需要使其可从 JavaScript 使用。
原生视图由 RCTViewManager
的子类创建和操作。这些子类的功能类似于视图控制器,但本质上是单例 - 桥接器只创建每个子类的单个实例。它们将原生视图暴露给 RCTUIManager
,后者将委托回它们以根据需要设置和更新视图的属性。RCTViewManager
通常也是视图的委托,通过桥接器将事件发送回 JavaScript。
要公开视图,您可以
- 子类化
RCTViewManager
以创建组件的管理器。 - 添加
RCT_EXPORT_MODULE()
标记宏。 - 实现
-(UIView *)view
方法。
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE(RNTMap)
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
不要尝试在您通过 -view
方法公开的 UIView
实例上设置 frame
或 backgroundColor
属性。React Native 将覆盖您的自定义类设置的值,以匹配您的 JavaScript 组件的布局属性。如果您需要这种粒度的控制,最好将您想要设置样式的 UIView
实例包装在另一个 UIView
中,并返回包装器 UIView
。有关更多上下文,请参阅问题 2948。
在上面的示例中,我们在类名前加了 RNT
前缀。前缀用于避免与其他框架发生名称冲突。Apple 框架使用两个字母的前缀,React Native 使用 RCT
作为前缀。为了避免名称冲突,我们建议您在自己的类中使用除 RCT
之外的三个字母的前缀。
然后您需要一些 JavaScript 代码才能将其变成可用的 React 组件
import {requireNativeComponent} from 'react-native';
export default requireNativeComponent('RNTMap');
requireNativeComponent
函数会自动将 RNTMap
解析为 RNTMapManager
,并将我们的原生视图导出以供 JavaScript 使用。
import MapView from './MapView.tsx';
export default function MyApp() {
return <MapView style={{flex: 1}} />;
}
渲染时,不要忘记拉伸视图,否则您将看到一个空白屏幕。
现在,这是一个在 JavaScript 中完全可用的原生地图视图组件,具有捏合缩放和其他原生手势支持。不过,我们还无法从 JavaScript 中真正控制它。
属性
我们可以做的第一件事是桥接一些原生属性,使这个组件更易用。假设我们想要能够禁用缩放并指定可见区域。禁用缩放是一个布尔值,因此我们添加这一行代码
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
请注意,我们明确地将类型指定为 BOOL
- React Native 在后台使用 RCTConvert
来转换各种不同的数据类型,当通过桥接器通信时,错误的值将显示方便的“RedBox”错误,让您知道尽快地存在问题。当事情像这样简单明了时,这个宏会为您处理整个实现。
现在要实际禁用缩放,我们在 JavaScript 中设置属性
import MapView from './MapView.tsx';
export default function MyApp() {
return <MapView zoomEnabled={false} style={{flex: 1}} />;
}
为了记录 MapView
组件的属性(以及它们接受哪些值),我们将添加一个包装器组件并使用 TypeScript 记录接口
import {requireNativeComponent} from 'react-native';
const RNTMap = requireNativeComponent('RNTMap');
export default function MapView(props: {
/**
* Whether the user may use pinch gestures to zoom in and out.
*/
zoomEnabled?: boolean;
}) {
return <RNTMap {...props} />;
}
现在我们有一个可以很好地记录的包装器组件可以使用了。
接下来,让我们添加更复杂的 region
属性。我们首先添加原生代码
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 向我们发送空哨兵,我们将使用它将属性重置回默认值。
您可以为您的视图编写任何您想要的转换函数 - 以下是通过 RCTConvert
上的类别实现 MKCoordinateRegion
的方式。它使用 ReactNative RCTConvert+CoreLocation
的一个已经存在的类别
#import "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
这些转换函数旨在安全地处理 JS 可能抛出的任何 JSON,方法是在遇到缺少键或其他开发者错误时显示“RedBox”错误并返回标准初始化值。
为了完成对 region
属性的支持,我们可以使用 TypeScript 记录它
import {requireNativeComponent} from 'react-native';
const RNTMap = requireNativeComponent('RNTMap');
export default function MapView(props: {
/**
* The region to be displayed by the map.
*
* The region is defined by the center coordinates and the span of
* coordinates to display.
*/
region?: {
/**
* Coordinates for the center of the map.
*/
latitude: number;
longitude: number;
/**
* Distance between the minimum and the maximum latitude/longitude
* to be displayed.
*/
latitudeDelta: number;
longitudeDelta: number;
};
/**
* Whether the user may use pinch gestures to zoom in and out.
*/
zoomEnabled?: boolean;
}) {
return <RNTMap {...props} />;
}
现在我们可以向 MapView
提供 region
属性了
import MapView from './MapView.tsx';
export default function MyApp() {
const region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{flex: 1}}
/>
);
}
事件
因此,现在我们有一个可以从 JS 自由控制的原生地图组件,但是我们如何处理来自用户的事件,例如捏合缩放或平移以更改可见区域呢?
到目前为止,我们只从管理器的 -(UIView *)view
方法返回了一个 MKMapView
实例。我们不能向 MKMapView
添加新属性,因此我们必须从 MKMapView
创建一个新的子类,我们将其用于我们的 View。然后,我们可以在此子类上添加一个 onRegionChange
回调
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMapView: MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
#import "RNTMapView.h"
@implementation RNTMapView
@end
请注意,所有 RCTBubblingEventBlock
都必须以 on
为前缀。接下来,在 RNTMapManager
上声明一个事件处理程序属性,使其成为其公开的所有视图的委托,并通过从原生视图调用事件处理程序块将事件转发到 JS。
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.h"
@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
// ...
type RegionChangeEvent = {
nativeEvent: {
latitude: number;
longitude: number;
latitudeDelta: number;
longitudeDelta: number;
};
};
export default function MapView(props: {
// ...
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: (event: RegionChangeEvent) => unknown;
}) {
return <RNTMap {...props} onRegionChange={onRegionChange} />;
}
import MapView from './MapView.tsx';
export default function MyApp() {
// ...
const onRegionChange = useCallback(event => {
const {region} = event.nativeEvent;
// Do something with `region.latitude`, etc.
});
return (
<MapView
// ...
onRegionChange={onRegionChange}
/>
);
}
处理多个原生视图
一个 React Native 视图可以在视图树中拥有多个子视图,例如:
<View>
<MyNativeView />
<MyNativeView />
<Button />
</View>
在此示例中,类 MyNativeView
是 NativeComponent
的包装器,并公开将在 iOS 平台上调用的方法。MyNativeView
在 MyNativeView.ios.js
中定义,并包含 NativeComponent
的代理方法。
当用户与组件交互时,例如点击按钮,MyNativeView
的 backgroundColor
会发生变化。在这种情况下,UIManager
将不知道应该处理哪个 MyNativeView
以及应该更改哪个 MyNativeView
的 backgroundColor
。您将在下面找到此问题的解决方案
<View>
<MyNativeView ref={this.myNativeReference} />
<MyNativeView ref={this.myNativeReference2} />
<Button
onPress={() => {
this.myNativeReference.callNativeMethod();
}}
/>
</View>
现在上面的组件对特定的 MyNativeView
有一个引用,这允许我们使用 MyNativeView
的特定实例。现在按钮可以控制哪个 MyNativeView
应该更改其 backgroundColor
。在此示例中,假设 callNativeMethod
会更改 backgroundColor
。
class MyNativeView extends React.Component {
callNativeMethod = () => {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.getViewManagerConfig('RNCMyNativeView').Commands
.callNativeMethod,
[],
);
};
render() {
return <NativeComponent ref={NATIVE_COMPONENT_REF} />;
}
}
callNativeMethod
是我们的自定义 iOS 方法,例如,它会更改通过 MyNativeView
公开的 backgroundColor
。此方法使用 UIManager.dispatchViewManagerCommand
,它需要 3 个参数
(nonnull NSNumber \*)reactTag
- React 视图的 ID。commandID:(NSInteger)commandID
- 应该调用的原生方法的 IDcommandArgs:(NSArray<id> \*)commandArgs
- 我们可以从 JS 传递到原生的原生方法的参数。
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTLog.h>
RCT_EXPORT_METHOD(callNativeMethod:(nonnull NSNumber*) reactTag) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
NativeView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[NativeView class]]) {
RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
return;
}
[view callNativeMethod];
}];
}
这里 callNativeMethod
在 RNCMyNativeViewManager.m
文件中定义,并且只有一个参数,即 (nonnull NSNumber*) reactTag
。此导出的函数将使用包含 viewRegistry
参数的 addUIBlock
查找特定的视图,并根据 reactTag
返回组件,从而允许它在正确的组件上调用方法。
样式
由于我们所有原生 React 视图都是 UIView
的子类,因此大多数样式属性都可以像您预期的那样开箱即用。但是,某些组件可能需要默认样式,例如 UIDatePicker
,它具有固定大小。此默认样式对于布局算法按预期工作非常重要,但我们也希望能够在使用组件时覆盖默认样式。DatePickerIOS
通过将原生组件包装在一个具有灵活样式的额外视图中,并在内部原生组件上使用固定样式(通过从原生传递的常量生成)来实现这一点。
import {UIManager} from 'react-native';
const RCTDatePickerIOSConsts = UIManager.RCTDatePicker.Constants;
...
render: function() {
return (
<View style={this.props.style}>
<RCTDatePickerIOS
ref={DATEPICKER}
style={styles.rkDatePickerIOS}
...
/>
</View>
);
}
});
const styles = StyleSheet.create({
rkDatePickerIOS: {
height: RCTDatePickerIOSConsts.ComponentHeight,
width: RCTDatePickerIOSConsts.ComponentWidth,
},
});
RCTDatePickerIOSConsts
常量通过从原生获取原生组件的实际框架来导出,如下所示
- (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),
}
};
}
本指南涵盖了桥接自定义原生组件的许多方面,但您可能还需要考虑更多方面,例如用于插入和布局子视图的自定义钩子。如果您想更深入地了解,请查看某些已实现组件的源代码。