【React Native】文件翻譯閱讀紀錄 - 指南(iOS) - 原生 UI 組件
Facebook Open Source React Native |
原生 UI 組件
有大量的本機UI小部件可以在最新的應用程序中使用 - 其中一些是平台的一部分,另一些可用作第三方庫,還有更多可能在您自己的產品組合中使用。 React Native已經包含了幾個最關鍵的平台組件,比如ScrollView和TextInput,但不是全部,而且肯定不是你自己為之前的應用編寫的。幸運的是,將這些現有組件包裝起來以便與React Native應用程序無縫集成非常容易。
與本機模塊指南一樣,這也是一個更高級的指南,假設您對iOS編程有點熟悉。本指南將向您展示如何構建本機UI組件,引導您完成核心React Native庫中可用的現有MapView組件的子集的實現。
與本機模塊指南一樣,這也是一個更高級的指南,假設您對iOS編程有點熟悉。本指南將向您展示如何構建本機UI組件,引導您完成核心React Native庫中可用的現有MapView組件的子集的實現。
iOS MapView 示例
假設我們想要在我們的應用程序中添加一個交互式地圖 - 不妨使用MKMapView,我們只需要讓它可以從JavaScript中使用。
本地視圖由RCTViewManager的子類創建和操作。這些子類在功能上與視圖控制器類似,但基本上是單例 - 每個橋只創建一個實例。它們將本機視圖公開給RCTUIManager,後者委託他們根據需要設置和更新視圖的屬性。 RCTViewManagers通常也是視圖的代表,通過網橋將事件發送回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中設置屬性:
現在要實際禁用縮放,我們在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 類別:
您可以為視圖編寫所需的任何轉換函數 - 這是通過 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中記錄它(或者我們將得到一個錯誤,原生道具未被記錄),然後我們可以像任何其他道具一樣設置它:
要完成對區域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選項,例如
有時,您的本機組件將具有一些特殊屬性,您不希望它們成為關聯的React組件的API的一部分。例如,Switch為原始本機事件提供了一個自定義onChange處理程序,並公開了一個onValueChange處理程序屬性,該屬性僅使用布爾值而不是原始事件來調用。由於您不希望這些僅本機屬性成為API的一部分,因此您不希望將它們放在propTypes中,但如果不這樣做,則會出現錯誤。解決方案只是將它們添加到nativeOnly選項,例如
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
nativeOnly: {onChange: true},
});
活動
所以現在我們有一個可以從JS輕鬆控制的本機地圖組件,但是我們如何處理來自用戶的事件,比如捏縮放或平移來改變可見區域?
到目前為止,我們剛剛從經理的 - (UIView *)視圖方法返回了一個 MKMapView實例。我們無法向 MKMapView 添加新屬性,因此我們必須從 MKMapView 創建一個新的子類,我們將其用於View。然後我們可以在這個子類上添加一個onRegionChange 回調:
到目前為止,我們剛剛從經理的 - (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),
}
};
}
0 意見