【React Native】文件翻譯閱讀紀錄 - 指南(iOS) - 原生 UI 組件

by - 上午9:00

Facebook Open Source React Native


原生 UI 組件

有大量的本機UI小部件可以在最新的應用程序中使用 - 其中一些是平台的一部分,另一些可用作第三方庫,還有更多可能在您自己的產品組合中使用。 React Native已經包含了幾個最關鍵的平台組件,比如ScrollView和TextInput,但不是全部,而且肯定不是你自己為之前的應用編寫的。幸運的是,將這些現有組件包裝起來以便與React Native應用程序無縫集成非常容易。

與本機模塊指南一樣,這也是一個更高級的指南,假設您對iOS編程有點熟悉。本指南將向您展示如何構建本機UI組件,引導您完成核心React Native庫中可用的現有MapView組件的子集的實現。

iOS MapView 示例

假設我們想要在我們的應用程序中添加一個交互式地圖 - 不妨使用MKMapView,我們只需要讓它可以從JavaScript中使用。

本地視圖由RCTViewManager的子類創建和操作。這些子類在功能上與視圖控制器類似,但基本上是單例 - 每個橋只創建一個實例。它們將本機視圖公開給RCTUIManager,後者委託他們根據需要設置和更新視圖的屬性。 RCTViewManagers通常也是視圖的代表,通過網橋將事件發送回JavaScript。


公開視圖很簡單:
  • 子類RCTViewManager為您的組件創建管理器。
  • 添加 RCT_EXPORT_MODULE() 標記。
  • 實現 -(UIView *) 視圖方法
// 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 
注意:不要嘗試在通過-view方法公開的UIView實例上設置frame或backgroundColor屬性。 React Native將覆蓋自定義類設置的值,以匹配JavaScript組件的佈局道具。如果您需要這種控製粒度,最好將要在另一個UIView中設置樣式的UIView實例包裝起來,然後返回包裝器UIView。有關更多上下文,請參見問題 Issue 2948
在上面的示例中,我們使用RNT為類名添加前綴。前綴用於避免與其他框架的名稱衝突。 Apple框架使用雙字母前綴,React Native使用RCT作為前綴。為了避免名稱衝突,我們建議在您自己的類中使用除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 與本機props匹配,以減少 ObjC 和 JS 代碼之間不匹配的可能性。

接下來,讓我們添加更複雜的區域道具。我們首先添加本機代碼:
// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
  [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
好吧,這比我們以前的簡單BOOL案例更複雜。現在我們有一個需要轉換函數的MKCoordinateRegion 類型,並且我們有自定義代碼,以便在我們從JS設置區域時視圖將生成動畫。在我們提供的函數體中,json引用從JS傳遞的原始值。還有一個視圖變量,它允許我們訪問管理器的視圖實例,以及一個defaultView,我們使用它來將屬性重置為默認值,如果JS向我們發送一個空的哨兵。

您可以為視圖編寫所需的任何轉換函數 - 這是通過 RCTConvert 上的類別實現MKCoordinateRegion。它使用已存在的 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。

要完成對區域prop的支持,我們需要在propTypes中記錄它(或者我們將得到一個錯誤,原生道具未被記錄),然後我們可以像任何其他道具一樣設置它:
// 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輕鬆控制的本機地圖組件,但是我們如何處理來自用戶的事件,比如捏縮放或平移來改變可見區域?

到目前為止,我們剛剛從經理的 - (UIView *)視圖方法返回了一個 MKMapView實例。我們無法向 MKMapView 添加新屬性,因此我們必須從 MKMapView 創建一個新的子類,我們將其用於View。然後我們可以在這個子類上添加一個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中調用相同的回調prop。使用raw事件調用此回調,我們通常在包裝器組件中處理該事件以生成更簡單的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),
    }
  };
}
本指南涵蓋了跨越自定義本機組件的許多方面,但您可能需要考慮更多,例如用於插入和佈置子視圖的自定義掛鉤。如果您想更深入,請查看一些已實現組件的源代碼。



You May Also Like

0 意見