Native UI 組件(iOS)

2019-08-14 14:21 更新

有許多 native UI 小部件可以應(yīng)用到最新的應(yīng)用程序中——其中一些是平臺(tái)的一部分,另外的可以用作第三方庫(kù),并且更多的是它們可以用于你自己的選集中。React Native 有幾個(gè)最關(guān)鍵的平臺(tái)組件已經(jīng)包裝好了,如 ScrollView TextInput,但不是所有的組件都被包裝好了,當(dāng)然了,你為先前的應(yīng)用程序?qū)懙慕M件肯定沒(méi)有包裝好。幸運(yùn)的是,為了與 React Native 應(yīng)用程序無(wú)縫集成,將現(xiàn)存組件包裝起來(lái)是非常容易實(shí)現(xiàn)的。

正如 native 模塊指南,這也是一種更高級(jí)的指南,假定你對(duì) iOS 編程有一定的了解。本指南將向你展示如何構(gòu)建一個(gè)本地的 UI 組件,帶你實(shí)現(xiàn)在核心 React Native 庫(kù)中可用的現(xiàn)存的 MapView 組件的子集。

iOS MapView 示例

如果說(shuō)我們想在我們的應(yīng)用程序中添加一個(gè)交互式的 Map——不妨用 MKMapView,我們只需要讓它在 JavaScript 中可用。

Native 視圖是通過(guò) RCTViewManager 的子類創(chuàng)建和操做的。這些子類的功能與視圖控制器很相似,但本質(zhì)上它們是單件模式——橋只為每一個(gè)子類創(chuàng)建一個(gè)實(shí)例。它們將 native 視圖提供給 RCTUIManager,它會(huì)傳回到 native 視圖來(lái)設(shè)置和更新的必要的視圖屬性。RCTViewManager 通常也是視圖的代表,通過(guò)橋?qū)⑹录l(fā)送回 JavaScript。

發(fā)送視圖是很簡(jiǎn)單的:

  • 創(chuàng)建基本的子類。

  • 添加標(biāo)記宏 RCT_EXPORT_MODULE()。

  • 實(shí)現(xiàn) -(UIView *)view 方法。

// RCTMapManager.m#import <MapKit/MapKit.h>#import "RCTViewManager.h"@interface RCTMapManager : RCTViewManager@end@implementation RCTMapManagerRCT_EXPORT_MODULE()
- (UIView *)view
{  return [[MKMapView alloc] init];
}@end

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

// MapView.jsvar { requireNativeComponent } = require('react-native');module.exports = requireNativeComponent('RCTMap', null);

現(xiàn)在這是 JavaScript 中一個(gè)功能完整的 native map 視圖組件了,包括 pinch-zoom 和其他 native 手勢(shì)支持。但是我們還不能用 JavaScript 來(lái)真正的控制它。

屬性

為了使該組件更可用,我們可以做的第一件事是連接一些 native 屬性。比如說(shuō)我們希望能夠禁用音高控制并指定可見(jiàn)區(qū)域。禁用音高是一個(gè)簡(jiǎn)單的布爾值,所以我們只添加這一行:

// RCTMapManager.mRCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)

注意我們顯式的指定類型為 BOOL——當(dāng)談到連接橋時(shí),React Native 使用 hood 下的 RCTConvert 來(lái)轉(zhuǎn)換所有不同的數(shù)據(jù)類型,且錯(cuò)誤的值會(huì)顯示明顯的 “RedBox” 錯(cuò)誤使你知道這里有 ASAP 問(wèn)題。當(dāng)一切進(jìn)展順利時(shí),這個(gè)宏就會(huì)為你處理整個(gè)實(shí)現(xiàn)。

現(xiàn)在要真正的實(shí)現(xiàn)禁用音高,我們只需要在 JS 中設(shè)置如下所示屬性:

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

但是這不是很好記錄——為了知道哪些屬性可用以及它們接收了什么值,你的新組件的客戶端需要挖掘 objective-C 代碼。為了更好的實(shí)現(xiàn)這一點(diǎn),讓我們做一個(gè)包裝器組件并用 React PropTypes 記錄接口:

// MapView.jsvar 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)在我們有一個(gè)很不錯(cuò)的已記錄的包裝器組件,它使用非常容易。注意我們?yōu)樾碌?nbsp;MapView 包裝器組件將第二個(gè)參數(shù)從null 改為 requireNativeComponent。這使得基礎(chǔ)設(shè)施驗(yàn)證了 propTypes 匹配native 工具來(lái)減少 ObjC 和 JS 代碼之間的不匹配的可能。

接下來(lái),讓我們添加更復(fù)雜的 region 工具。從添加 native 代碼入手:

// RCTMapManager.mRCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
{
  [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}

好的,這顯然比之前簡(jiǎn)單的 BOOL 情況更加復(fù)雜?,F(xiàn)在我們有一個(gè) MKCoordinateRegion 類型,該類型需要一個(gè)轉(zhuǎn)換函數(shù),并且我們有自定義的代碼,這樣當(dāng)我們從 JS 設(shè)置區(qū)域時(shí),視圖可以產(chǎn)生動(dòng)畫效果。還有一個(gè) defaultView,如果 JS 發(fā)送給我們一個(gè) null 標(biāo)記,我們使用它將屬性重置回默認(rèn)值。

當(dāng)然你可以為你的視圖編寫任何你想要的轉(zhuǎn)換函數(shù)——下面是通過(guò) RCTConvert 的兩類來(lái)實(shí)現(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]
  };
}

這些轉(zhuǎn)換函數(shù)是為了安全地處理任何 JSON 而設(shè)計(jì)的,當(dāng)出現(xiàn)丟失的鍵或開發(fā)人員錯(cuò)誤操作時(shí),JS 可能向它們拋出 “RedBox” 錯(cuò)誤并返回標(biāo)準(zhǔn)的初始化值。

為完成對(duì) region 工具的支持,我們需要把它記錄到 propTypes中(否則我們將得到一個(gè)錯(cuò)誤,即 native 工具沒(méi)有被記錄),然后我們就可以按照設(shè)置其他工具的方式來(lái)設(shè)置它:

// MapView.jsMapView.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 文檔中是顯式的——理想情況下我們可以生成一些這方面的東西,但是這沒(méi)有實(shí)現(xiàn)。

事件

所以現(xiàn)在我們有一個(gè) native map 組件,可以從 JS 很容易的控制,但是我們?nèi)绾翁幚韥?lái)自用戶的事件,如 pinch-zooms 或平移來(lái)改變可見(jiàn)區(qū)域?關(guān)鍵是要使 RCTMapManager 成為它發(fā)送的所有視圖的代表,并把事件通過(guò)事件調(diào)度器發(fā)送給 JS。這看起來(lái)如下所示(從整個(gè)實(shí)現(xiàn)中簡(jiǎn)化出來(lái)的部分):

// RCTMapManager.m#import "RCTMapManager.h"#import <MapKit/MapKit.h>#import "RCTBridge.h"#import "RCTEventDispatcher.h"#import "UIView+React.h"@interface RCTMapManager() <MKMapViewDelegate>@end@implementation RCTMapManagerRCT_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];
}

你可以看到我們?cè)O(shè)置管理器為它發(fā)送的每個(gè)視圖的代表,然后在代表方法 -mapView:regionDidChangeAnimated: 中,區(qū)域與 reactTag 目標(biāo)相結(jié)合來(lái)產(chǎn)生事件,通過(guò) sendInputEventWithName:body 分派到你應(yīng)用程序中相應(yīng)的 React 組件實(shí)例中。事件名稱 @"topChange" 映射到從 JavaScript 中回調(diào)的 onChange這里查看 mappings )。原始事件調(diào)用這個(gè)回調(diào),我們通常在包裝器組件中處理這個(gè)過(guò)程來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 API:

// MapView.jsclass 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ù)樣式屬性會(huì)像你預(yù)想的一樣內(nèi)存不足。然而,一些組件需要默認(rèn)的樣式,例如 UIDatePicker,大小固定。為了達(dá)到預(yù)期的效果,默認(rèn)樣式對(duì)布局算法來(lái)說(shuō)是非常重要的,但是我們也希望在使用組件時(shí)能夠覆蓋默認(rèn)的樣式。DatePickerIOS 通過(guò)包裝一個(gè)額外的視圖中的 native 組件實(shí)現(xiàn)這一功能,該額外的視圖具有靈活的樣式設(shè)計(jì),并在內(nèi)部 native 組件中使用一個(gè)固定的樣式(用從 native 傳遞的常量生成):

// DatePickerIOS.ios.jsvar 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 常量是通過(guò)抓取 native 組件的實(shí)際框架從 native 中導(dǎo)出的,如下所示:

// 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 來(lái)插入和布局子視圖。如果你想了解更多,請(qǐng)?jiān)?a rel="nofollow" href="http://www.o2fo.com/targetlink?url=https://github.com/facebook/react-native/blob/master/React/Views" target="_blank" style="box-sizing: border-box; font-family: Verdana, 'Lantinghei SC', 'Hiragino Sans GB', 'Microsoft Yahei', Helvetica, arial, 宋體, sans-serif; background-color: transparent; color: rgb(45, 133, 202); text-decoration: none;">源代碼中查看實(shí)際的 RCTMapManager 和其他組件。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)