Native UI 组件(iOS)

有许多 native UI 小部件可以应用到最新的应用程序中——其中一些是平台的一部分,另外的可以用作第三方库,并且更多的是它们可以用于你自己的选集中。React Native 有几个最关键的平台组件已经包装好了,如 ScrollViewTextInput,但不是所有的组件都被包装好了,当然了,你为先前的应用程序写的组件肯定没有包装好。幸运的是,为了与 React Native 应用程序无缝集成,将现存组件包装起来是非常容易实现的。

正如 native 模块指南,这也是一种更高级的指南,假定你对 iOS 编程有一定的了解。本指南将向你展示如何构建一个本地的 UI 组件,带你实现在核心 React Native 库中可用的现存的 MapView 组件的子集。

iOS MapView 示例

如果说我们想在我们的应用程序中添加一个交互式的 Map——不妨用 MKMapView,我们只需要让它在 JavaScript 中可用。

Native 视图是通过 RCTViewManager 的子类创建和操做的。这些子类的功能与视图控制器很相似,但本质上它们是单件模式——桥只为每一个子类创建一个实例。它们将 native 视图提供给 RCTUIManager,它会传回到 native 视图来设置和更新的必要的视图属性。RCTViewManager 通常也是视图的代表,通过桥将事件发送回 JavaScript。

发送视图是很简单的:

  • 创建基本的子类。

  • 添加标记宏 RCT_EXPORT_MODULE()

  • 实现 -(UIView *)view 方法。

  1. // RCTMapManager.m
  2. #import <MapKit/MapKit.h>
  3. #import "RCTViewManager.h"
  4. @interface RCTMapManager : RCTViewManager
  5. @end
  6. @implementation RCTMapManager
  7. RCT_EXPORT_MODULE()
  8. - (UIView *)view
  9. {
  10. return [[MKMapView alloc] init];
  11. }
  12. @end

然后你需要一些 JavaScript 使之成为有用的 React 组件:

  1. // MapView.js
  2. var { requireNativeComponent } = require('react-native');
  3. module.exports = requireNativeComponent('RCTMap', null);

现在这是 JavaScript 中一个功能完整的 native map 视图组件了,包括 pinch-zoom 和其他 native 手势支持。但是我们还不能用 JavaScript 来真正的控制它。

属性

为了使该组件更可用,我们可以做的第一件事是连接一些 native 属性。比如说我们希望能够禁用音高控制并指定可见区域。禁用音高是一个简单的布尔值,所以我们只添加这一行:

  1. // RCTMapManager.m
  2. RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)

注意我们显式的指定类型为 BOOL——当谈到连接桥时,React Native 使用 hood 下的 RCTConvert 来转换所有不同的数据类型,且错误的值会显示明显的 “RedBox” 错误使你知道这里有 ASAP 问题。当一切进展顺利时,这个宏就会为你处理整个实现。

现在要真正的实现禁用音高,我们只需要在 JS 中设置如下所示属性:

  1. // MyApp.js
  2. <MapView pitchEnabled={false} />

但是这不是很好记录——为了知道哪些属性可用以及它们接收了什么值,你的新组件的客户端需要挖掘 objective-C 代码。为了更好的实现这一点,让我们做一个包装器组件并用 React PropTypes 记录接口:

  1. // MapView.js
  2. var React = require('react-native');
  3. var { requireNativeComponent } = React;
  4. class MapView extends React.Component {
  5. render() {
  6. return <RCTMap {...this.props} />;
  7. }
  8. }
  9. var RCTMap = requireNativeComponent('RCTMap', MapView);
  10. MapView.propTypes = {
  11. /**
  12. * When this property is set to `true` and a valid camera is associated
  13. * with the map, the camera’s pitch angle is used to tilt the plane
  14. * of the map. When this property is set to `false`, the camera’s pitch
  15. * angle is ignored and the map is always displayed as if the user
  16. * is looking straight down onto it.
  17. */
  18. pitchEnabled: React.PropTypes.bool,
  19. };
  20. module.exports = MapView;

现在我们有一个很不错的已记录的包装器组件,它使用非常容易。注意我们为新的 MapView 包装器组件将第二个参数从 null 改为 requireNativeComponent。这使得基础设施验证了 propTypes 匹配native 工具来减少 ObjC 和 JS 代码之间的不匹配的可能。

接下来,让我们添加更复杂的 region 工具。从添加 native 代码入手:

  1. // RCTMapManager.m
  2. RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
  3. {
  4. [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
  5. }

好的,这显然比之前简单的 BOOL 情况更加复杂。现在我们有一个 MKCoordinateRegion 类型,该类型需要一个转换函数,并且我们有自定义的代码,这样当我们从 JS 设置区域时,视图可以产生动画效果。还有一个 defaultView,如果 JS 发送给我们一个 null 标记,我们使用它将属性重置回默认值。

当然你可以为你的视图编写任何你想要的转换函数——下面是通过 RCTConvert 的两类来实现 MKCoordinateRegion 的例子:

  1. @implementation RCTConvert(CoreLocation)
  2. RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue);
  3. RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);
  4. + (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json
  5. {
  6. json = [self NSDictionary:json];
  7. return (CLLocationCoordinate2D){
  8. [self CLLocationDegrees:json[@"latitude"]],
  9. [self CLLocationDegrees:json[@"longitude"]]
  10. };
  11. }
  12. @end
  13. @implementation RCTConvert(MapKit)
  14. + (MKCoordinateSpan)MKCoordinateSpan:(id)json
  15. {
  16. json = [self NSDictionary:json];
  17. return (MKCoordinateSpan){
  18. [self CLLocationDegrees:json[@"latitudeDelta"]],
  19. [self CLLocationDegrees:json[@"longitudeDelta"]]
  20. };
  21. }
  22. + (MKCoordinateRegion)MKCoordinateRegion:(id)json
  23. {
  24. return (MKCoordinateRegion){
  25. [self CLLocationCoordinate2D:json],
  26. [self MKCoordinateSpan:json]
  27. };
  28. }

这些转换函数是为了安全地处理任何 JSON 而设计的,当出现丢失的键或开发人员错误操作时,JS 可能向它们抛出 “RedBox” 错误并返回标准的初始化值。

为完成对 region 工具的支持,我们需要把它记录到 propTypes中(否则我们将得到一个错误,即 native 工具没有被记录),然后我们就可以按照设置其他工具的方式来设置它:

  1. // MapView.js
  2. MapView.propTypes = {
  3. /**
  4. * When this property is set to `true` and a valid camera is associated
  5. * with the map, the camera’s pitch angle is used to tilt the plane
  6. * of the map. When this property is set to `false`, the camera’s pitch
  7. * angle is ignored and the map is always displayed as if the user
  8. * is looking straight down onto it.
  9. */
  10. pitchEnabled: React.PropTypes.bool,
  11. /**
  12. * The region to be displayed by the map.
  13. *
  14. * The region is defined by the center coordinates and the span of
  15. * coordinates to display.
  16. */
  17. region: React.PropTypes.shape({
  18. /**
  19. * Coordinates for the center of the map.
  20. */
  21. latitude: React.PropTypes.number.isRequired,
  22. longitude: React.PropTypes.number.isRequired,
  23. /**
  24. * Distance between the minimum and the maximum latitude/longitude
  25. * to be displayed.
  26. */
  27. latitudeDelta: React.PropTypes.number.isRequired,
  28. longitudeDelta: React.PropTypes.number.isRequired,
  29. }),
  30. };
  31. // MyApp.js
  32. render() {
  33. var region = {
  34. latitude: 37.48,
  35. longitude: -122.16,
  36. latitudeDelta: 0.1,
  37. longitudeDelta: 0.1,
  38. };
  39. return <MapView region={region} />;
  40. }

在这里你可以看到该区域的形状在 JS 文档中是显式的——理想情况下我们可以生成一些这方面的东西,但是这没有实现。

事件

所以现在我们有一个 native map 组件,可以从 JS 很容易的控制,但是我们如何处理来自用户的事件,如 pinch-zooms 或平移来改变可见区域?关键是要使 RCTMapManager 成为它发送的所有视图的代表,并把事件通过事件调度器发送给 JS。这看起来如下所示(从整个实现中简化出来的部分):

  1. // RCTMapManager.m
  2. #import "RCTMapManager.h"
  3. #import <MapKit/MapKit.h>
  4. #import "RCTBridge.h"
  5. #import "RCTEventDispatcher.h"
  6. #import "UIView+React.h"
  7. @interface RCTMapManager() <MKMapViewDelegate>
  8. @end
  9. @implementation RCTMapManager
  10. RCT_EXPORT_MODULE()
  11. - (UIView *)view
  12. {
  13. MKMapView *map = [[MKMapView alloc] init];
  14. map.delegate = self;
  15. return map;
  16. }
  17. #pragma mark MKMapViewDelegate
  18. - (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated
  19. {
  20. MKCoordinateRegion region = mapView.region;
  21. NSDictionary *event = @{
  22. @"target": mapView.reactTag,
  23. @"region": @{
  24. @"latitude": @(region.center.latitude),
  25. @"longitude": @(region.center.longitude),
  26. @"latitudeDelta": @(region.span.latitudeDelta),
  27. @"longitudeDelta": @(region.span.longitudeDelta),
  28. }
  29. };
  30. [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event];
  31. }

你可以看到我们设置管理器为它发送的每个视图的代表,然后在代表方法 -mapView:regionDidChangeAnimated: 中,区域与 reactTag 目标相结合来产生事件,通过 sendInputEventWithName:body 分派到你应用程序中相应的 React 组件实例中。事件名称 @"topChange" 映射到从 JavaScript 中回调的 onChange这里查看 mappings )。原始事件调用这个回调,我们通常在包装器组件中处理这个过程来实现一个简单的 API:

  1. // MapView.js
  2. class MapView extends React.Component {
  3. constructor() {
  4. this._onChange = this._onChange.bind(this);
  5. }
  6. _onChange(event: Event) {
  7. if (!this.props.onRegionChange) {
  8. return;
  9. }
  10. this.props.onRegionChange(event.nativeEvent.region);
  11. }
  12. render() {
  13. return <RCTMap {...this.props} onChange={this._onChange} />;
  14. }
  15. }
  16. MapView.propTypes = {
  17. /**
  18. * Callback that is called continuously when the user is dragging the map.
  19. */
  20. onRegionChange: React.PropTypes.func,
  21. ...
  22. };

样式

由于我们所有的 native react 视图是 UIView 的子类,大多数样式属性会像你预想的一样内存不足。然而,一些组件需要默认的样式,例如 UIDatePicker,大小固定。为了达到预期的效果,默认样式对布局算法来说是非常重要的,但是我们也希望在使用组件时能够覆盖默认的样式。DatePickerIOS 通过包装一个额外的视图中的 native 组件实现这一功能,该额外的视图具有灵活的样式设计,并在内部 native 组件中使用一个固定的样式(用从 native 传递的常量生成):

  1. // DatePickerIOS.ios.js
  2. var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants;
  3. ...
  4. render: function() {
  5. return (
  6. <View style={this.props.style}>
  7. <RCTDatePickerIOS
  8. ref={DATEPICKER}
  9. style={styles.rkDatePickerIOS}
  10. ...
  11. />
  12. </View>
  13. );
  14. }
  15. });
  16. var styles = StyleSheet.create({
  17. rkDatePickerIOS: {
  18. height: RCTDatePickerIOSConsts.ComponentHeight,
  19. width: RCTDatePickerIOSConsts.ComponentWidth,
  20. },
  21. });

RCTDatePickerIOSConsts 常量是通过抓取 native 组件的实际框架从 native 中导出的,如下所示:

  1. // RCTDatePickerManager.m
  2. - (NSDictionary *)constantsToExport
  3. {
  4. UIDatePicker *dp = [[UIDatePicker alloc] init];
  5. [dp layoutIfNeeded];
  6. return @{
  7. @"ComponentHeight": @(CGRectGetHeight(dp.frame)),
  8. @"ComponentWidth": @(CGRectGetWidth(dp.frame)),
  9. @"DatePickerModes": @{
  10. @"time": @(UIDatePickerModeTime),
  11. @"date": @(UIDatePickerModeDate),
  12. @"datetime": @(UIDatePickerModeDateAndTime),
  13. }
  14. };
  15. }

本指南涵盖了衔接自定义 native 组件的许多方面,但有你可能有更多需要考虑的地方,如自定义 hooks 来插入和布局子视图。如果你想了解更多,请在源代码中查看实际的 RCTMapManager 和其他组件。