原生组件

iOS 原生 UI 组件

本指南以 core React Native library 中已存在的 MapView 为例来介绍如何构建一个原生 UI 组件。

接下来对原生的 MKMapView 进行封装,使它可以在 JavaScript 中使用。

原生视图 (native view)RCTViewManager 的子类创建和操作。这些子类在功能上与 UIViewController 相似,但本质上是单例的,每个类只有一个由 bridge 创建的实例。它们把原生视图暴露给 RCTUIManagerRCTUIManager 委托它们在必要时设置和更新视图的属性。RCTViewManager 通常也是视图的 delegate ,通过 bridge 将事件发送回 JavaScript 。

导出原生 UI 组件的步骤:

  • 子类化 RCTViewManager ,为你的组件创建一个 manager;

  • 添加 RCT_EXPORT_MODULE() 宏;

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

在 Xcode 中添加

RNTMapManager.h

#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>

@interface RNTMapManager : RCTViewManager

@end

RNTMapManager.m

#import "RNTMapManager.h"

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

- (UIView *)view
{
    return [[MKMapView alloc] init];
}

@end

不要在 - (UIView *)view 导出的 view 实例上设置 framebackgroundColor 等属性,因为 React Native 会覆盖你设置的值,最终生效的是 JavaScript 组件设置的 layout props 。如果你需要更细的控制粒度,最好将你想样式化的 UIView 实例包装在另一个 UIView 中,并返回这个 wrapper UIView 。参考 React Native issue 2948

在 VS Code 中添加

MapView.js

注意:不可在 MapView.js 中执行 ⌘S ,否则会报错:“There are two approaches to error handling with callbacks” 。原因是 requireNativeComponent 被执行了两次。如果报错了,可在其他文件(比如 App.js )执行 ⌘S 或在 Metro 终端任务中输入 r 来刷新项目。

import { requireNativeComponent } from 'react-native';

// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap');

App.js

import React, { Component } from 'react';
import MapView from './MapView.js';

class App extends Component {
  render() {
    return <MapView style={{flex: 1}} />;
  }
}

export default App;

这现在是 JavaScript 中的一个功能齐全的原生地图视图组件,包含缩放和其他本地手势支持。但我们还不能从 JavaScript 控制它 :(

导出属性

要使该组件更可用,我们可以做的第一件事是在一些原生属性上进行桥接。比如我们想禁用缩放手势指定可视区域

示例一:导出 zoomEnabled 属性

缩放手势通过 zoomEnabled 属性控制,这是一个布尔值,在 RNTMapManager.m 中添加:

RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

其中 RCT_EXPORT_VIEW_PROPERTY(name, type) 宏的本质是一个方法:

/**
 * This handles the simple case, where JS and native property names match.
 */
#define RCT_EXPORT_VIEW_PROPERTY(name, type)            \
+(NSArray<NSString *> *)propConfig_##name RCT_DYNAMIC \
{                                                     \
  return @[ @ #type ];                                \
}

在 JS 中使用 MapView 时,就能设置 zoomEnabled 属性了。在 JS 中禁用缩放:

<MapView zoomEnabled={false} style={{flex: 1}} />;

为了记录 MapView 组件的属性(以及它们接受的值),我们将添加一个 wrapper 组件,并使用 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');

module.exports = MapView;

示例二:导出 region 属性

接下来,我们再添加一个更复杂的 region prop ,在 RNTMapManager.m 中添加:

RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
    MKCoordinateRegion myRegion = json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region;
    [view setRegion:myRegion animated:YES];
}

在上述代码中,json 是从 JS 传入的原始值。还有一个 view 变量,它允许我们访问 manager 的 view 实例,最后是 defaultView 变量,如果 JS 给我们发送一个空值,我们使用它将属性重置为默认值。

其中 RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) 宏的本质是包装了一个方法头,提供了 jsonviewdefaultView 三个属性给后面的方法体使用:

/**
 * This macro maps a named property to an arbitrary key path in the view.
 */
#define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type)    \
  +(NSArray<NSString *> *)propConfig_##name RCT_DYNAMIC \
  {                                                     \
    return @[ @ #type, @ #keyPath ];                    \
  }

/**
 * This macro can be used when you need to provide custom logic for setting
 * view properties. The macro should be followed by a method body, which can
 * refer to "json", "view" and "defaultView" to implement the required logic.
 */
#define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \
  RCT_REMAP_VIEW_PROPERTY(name, __custom__, type)       \
  -(void)set_##name : (id)json forView : (viewClass *)view withDefaultView : (viewClass *)defaultView RCT_DYNAMIC

RNTMapManager.m 中添加一个 RCTConvert (Mapkit) 分类,提供一个 + MKCoordinateRegion: 方法来把 JS 传入的 json 转成 MKCoordinateRegion ,在这个分类中使用了 React Native 库中已有的 RCTConvert+CoreLocation 分类中的方法:

#import "RNTMapManager.h"
#import <React/RCTConvert.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>

@interface RCTConvert (Mapkit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;

@end

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
    MKCoordinateRegion myRegion = json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region;
    [view setRegion:myRegion animated:YES];
}

- (UIView *)view
{
    return [[MKMapView alloc] init];
}

@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

MapView.jspropTypes 中添加 region

// 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,

    /**
     * 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,
    }),
  };


var RNTMap = requireNativeComponent('RNTMap');

module.exports = MapView;

App.js 中为 MapView 设置 region 属性:

import React, { Component } from 'react';
import MapView from './MapView.js';

class App extends Component {
  render() {
    var region = {
      latitude: 39.95,
      longitude: 116.31,
      latitudeDelta: 0.02,
      longitudeDelta: 0.02,
    };
    return (
      <MapView
        zoomEnabled={false}
        region={region}
        style={{ flex: 1 }}
      />
    );
  }
}

export default App;

导出事件的回调

现在我们有了一个原生的 map 组件,我们可以从 JS 中自由控制它,但是我们如何处理来自用户的事件,比如当用户缩放或平移 map 来改变可见区域时,如何将事件传递给 JS ?

在之前的代码中,我们在 manager 的 - (UIView *)view 方法中返回的是 MKMapView 的实例,由于我们无法往 MKMapView 中添加新属性,因此需要新建一个 MKMapView 的子类 RNTMapView ,然后在这个类中添加名为 onRegionChangeRCTBubblingEventBlock 类型的属性。

要注意的是,所有的 RCTBubblingEventBlock 类型的属性,必须以 on 开头。

#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>

@interface RNTMapView : MKMapView

@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;

@end

RNTMapManager 的修改:

  • RNTMapManager 中使用 RCT_EXPORT_VIEW_PROPERTY 宏添加 onRegionChange 属性;

  • - (UIView *)view 中返回 RNTMapView 的实例;

  • RNTMapManager 类遵守 MKMapViewDelegate 协议,在 - mapView:regionDidChangeAnimated: 中调用 onRegionChange 回调即可调用 JavaScript 中对应的回调 prop ;

#import "RNTMapManager.h"
#import "RNTMapView.h"
#import <React/RCTConvert.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>

@interface RCTConvert (Mapkit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;

@end

@interface RNTMapManager () <MKMapViewDelegate>

@end

@implementation RNTMapManager

RCT_EXPORT_MODULE(RNTMap)

RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)

RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
    MKCoordinateRegion myRegion = json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region;
    [view setRegion:myRegion animated:YES];
}

- (UIView *)view
{
    RNTMapView *mapView = [RNTMapView new];
    mapView.delegate = self;
    return mapView;
}

#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

@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

MapView.js 的类和 propTypes 中添加 onRegionChange 相关逻辑:

// MapView.js
import PropTypes from 'prop-types';
import React from 'react';
import { requireNativeComponent } from 'react-native';

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 = {
    /**
     * 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,

    /**
     * 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,
    }),
  };


var RNTMap = requireNativeComponent('RNTMap');

module.exports = MapView;

App.js 中添加 onRegionChange

import React, { Component } from 'react';
import MapView from './MapView.js';

class App extends Component {
  onRegionChange(event) {
    // Do stuff with event.region.latitude, etc.

  }

  render() {
    var region = {
      latitude: 39.95,
      longitude: 116.31,
      latitudeDelta: 0.02,
      longitudeDelta: 0.02,
    };
    return (
      <MapView
        zoomEnabled={false}
        region={region}
        onRegionChange={this.onRegionChange}
        style={{ flex: 1 }}
      />
    );
  }
}

export default App;

Handling multiple native views

https://reactnative.dev/docs/native-components-ios#handling-multiple-native-views

Styles

https://reactnative.dev/docs/native-components-ios#styles

Direct Manipulation

https://reactnative.dev/docs/direct-manipulation#composite-components-and-setnativeprops

Last updated

Was this helpful?