Компоненты пользовательского интерфейса iOS Native
В последних приложениях доступно множество встроенных элементов пользовательского интерфейса — некоторые из них являются частью платформы, другие — сторонними библиотеками, а ещё больше могут использоваться в вашем собственном портфолио. React Native уже обернул несколько важнейших компонентов платформы, таких как ScrollView и TextInput, но не все, и, безусловно, не те, которые вы могли написать сами для предыдущего приложения. К счастью, мы можем обернуть эти существующие компоненты для бесшовной интеграции с вашим приложением React Native.
Как и в руководстве по нативным модулям, это более продвинутое руководство, предполагающее, что вы в некоторой степени знакомы с программированием на iOS. Это руководство покажет вам, как создать компонент нативного пользовательского интерфейса, проведя вас через реализацию подмножества существующего компонента MapView, доступного в основной библиотеке React Native.
Пример iOS MapView
Предположим, мы хотим добавить интерактивную карту в наше приложение — можно использовать MKMapView, нам нужно только сделать его доступным из JavaScript.
Нативные представления создаются и обрабатываются подклассами RCTViewManager. Эти подклассы похожи по функциям на контроллеры представлений, но по сути являются синглтонами — только один экземпляр каждого создаётся мостом. Они предоставляют нативные представления RCTUIManager, которые делегируют им установку и обновление свойств представлений по мере необходимости. RCTViewManager также обычно являются делегатами представлений, отправляя события обратно в JavaScript через мост.
Чтобы предоставить представление, вы можете:
- Создать менеджер для своего компонента, наследовавшись от
RCTViewManager. - Добавить макрос маркера
RCT_EXPORT_MODULE(). - Реализовать метод
-(UIView *)view.
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE(RNTMap)
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
Примечание: Не пытайтесь установить свойства frame или backgroundColor на экземпляре UIView, который вы предоставляете через метод -view. React Native перезапишет значения, установленные вашим пользовательским классом, чтобы соответствовать свойствам макета вашего компонента JavaScript. Если вам нужна такая точность управления, возможно, лучше обернуть экземпляр UIView, который вы хотите стилизовать, в другой UIView и вернуть обернутый экземпляр UIView вместо этого. См. вопрос 2948 для получения дополнительной информации.
Информация: В приведённом выше примере мы добавили префикс к имени нашего класса RNT. Префиксы используются для предотвращения конфликтов имён с другими фреймворками. Фреймворки Apple используют двухбуквенные префиксы, а React Native использует RCT в качестве префикса. Чтобы избежать конфликтов имён, рекомендуется использовать трёхбуквенный префикс, отличный от RCT в ваших собственных классах.
Затем вам потребуется немного JavaScript-кода, чтобы сделать это компонентом React:
MapView.jsimport { requireNativeComponent } from 'react-native';
// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap');
MyApp.jsimport MapView from './MapView.js';
...
render() {
return <MapView style={{ flex: 1 }} />;
}
Убедитесь, что вы используете RNTMap здесь. Мы хотим потребовать менеджера здесь, что позволит предоставить представление нашего менеджера для использования в JavaScript.
Примечание: При отрисовке не забудьте растянуть представление, иначе вы будете смотреть на пустой экран.
render() {
return <MapView style={{flex: 1}} />;
}
Теперь у нас есть полностью работоспособный компонент нативной карты в JavaScript, с поддержкой масштабирования и других нативных жестов. Однако пока мы не можем управлять им из JavaScript :(
Свойства
Первое, что мы можем сделать, чтобы сделать этот компонент более удобным, — это передать некоторые нативные свойства. Предположим, мы хотим отключить масштабирование и указать видимую область. Отключение масштабирования — это булево значение, поэтому мы добавляем эту строчку:
RNTMapManager.mRCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
Обратите внимание, что мы явно указываем тип как BOOL — React Native использует RCTConvert под капотом для преобразования всех типов данных при общении через мост, а плохие значения будут показывать удобные ошибки "RedBox", чтобы немедленно сообщить вам о проблеме. В таких простых случаях вся реализация выполняется для вас этим макросом.
Теперь, чтобы фактически отключить масштабирование, мы устанавливаем свойство в JS:
MyApp.js<MapView zoomEnabled={false} style={{ flex: 1 }} />
Чтобы задокументировать свойства (и какие значения они принимают) нашего компонента MapView, мы добавим оберточный компонент и задокументируем интерфейс с помощью React PropTypes:
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. Мы начинаем с добавления кода нативному:
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
Хорошо, это сложнее, чем случай с BOOL раньше. Теперь у нас есть тип MKCoordinateRegion, который требует функции преобразования, и у нас есть пользовательский код, чтобы представление анимировалось при установке области из JS. Внутри функции, которую мы предоставляем, json относится к необработанному значению, переданному из JS. Также есть переменная view, которая даёт нам доступ к экземпляру представления менеджера, и defaultView, который мы используем для сброса свойства до значения по умолчанию, если JS отправляет нам нулевой маркер.
Вы можете написать любую функцию преобразования для вашего представления — вот реализация MKCoordinateRegion с помощью категории для RCTConvert . Она использует уже существующую категорию ReactNative RCTConvert+CoreLocation:
#import "RCTConvert+Mapkit.h"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
Эти функции преобразования предназначены для безопасной обработки любого JSON, который JS может передать им, отображая ошибки "RedBox" и возвращая стандартные значения инициализации при отсутствии ключей или других ошибках разработчика.
Чтобы закончить поддержку свойства region , нам нужно задокументировать его в propTypes:
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.jsrender() {
var region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{ flex: 1 }}
/>
);
}
Здесь вы видите, что форма области явно указана в JS-документации.
События
Итак, теперь у нас есть нативный компонент карты, которым мы можем свободно управлять из JS, но как мы обрабатываем события пользователя, такие как масштабирование или изменение видимой области при перемещении?
До сих пор мы возвращали только экземпляр MKMapView из метода -(UIView *)view нашего менеджера. Мы не можем добавлять новые свойства к MKMapView, поэтому нам нужно создать новый подкласс от MKMapView, который мы используем для нашего представления. Затем мы можем добавить обратный вызов onRegionChange в этот подкласс:
#import <MapKit/MapKit.h> #import <React/RCTComponent.h> @interface RNTMapView: MKMapView @property (nonatomic, copy) RCTBubblingEventBlock onRegionChange; @endRNTMapView.m
#import "RNTMapView.h" @implementation RNTMapView @end
Обратите внимание, что все RCTBubblingEventBlock должны быть с префиксом on Далее объявите свойство обработчика событий в RNTMapManager, сделайте его делегатом всех представлений, которые он предоставляет, и перенаправьте события в JS, вызвав блок обработчика событий из нативного представления.
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
#import "RNTMapView.h"
#import "RCTConvert+Mapkit.h"
@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. Этот обратный вызов вызывается с необработанным событием, которое мы обычно обрабатываем в оберточном компоненте для упрощения API:
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.jsclass 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}
/>
);
}
}
Обработка нескольких нативных представлений
Представление React Native может иметь более одного дочернего представления в дереве представлений, например:
<View> <MyNativeView /> <MyNativeView /> <Button /> </View>
В этом примере класс MyNativeView является оболочкой для NativeComponent и предоставляет методы, которые будут вызываться на платформе iOS. MyNativeView определён в MyNativeView.ios.js и содержит методы проксирования для NativeComponent.
Когда пользователь взаимодействует с компонентом, например, нажав кнопку, backgroundColor MyNativeView изменяется. В этом случае UIManager не знал бы, какое MyNativeView должно обрабатываться, а какое — изменить backgroundColor.
<View>
<MyNativeView ref={this.myNativeReference} />
<MyNativeView ref={this.myNativeReference2} />
<Button
onPress={() => {
this.myNativeReference.callNativeMethod();
}}
/>
</View>
Теперь вышеприведённый компонент имеет ссылку на определённое MyNativeView, что позволяет использовать конкретный экземпляр MyNativeView . Теперь кнопка может управлять тем, какое MyNativeView должно изменить своё backgroundColor . В этом примере предположим, что callNativeMethod изменяет backgroundColor.
class MyNativeView extends React.Component {
callNativeMethod = () => {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.getViewManagerConfig('RNCMyNativeView').Commands
.callNativeMethod,
[]
);
};
render() {
return <NativeComponent ref={NATIVE_COMPONENT_REF} />;
}
}
callNativeMethod — это наш пользовательский метод iOS, который, например, изменяет backgroundColor, доступный через MyNativeView . Этот метод использует UIManager.dispatchViewManagerCommand, которому требуется 3 параметра:
-
(nonnull NSNumber \*)reactTag— идентификатор представления React. -
commandID:(NSInteger)commandID— идентификатор нативного метода, который должен быть вызван. -
commandArgs:(NSArray<id> \*)commandArgs— аргументы нативного метода, которые мы можем передавать из JS в нативную часть.
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTLog.h>
RCT_EXPORT_METHOD(callNativeMethod:(nonnull NSNumber*) reactTag) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
NativeView *view = viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[NativeView class]]) {
RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
return;
}
[view callNativeMethod];
}];
}
Здесь callNativeMethod определён в файле RNCMyNativeViewManager.m и содержит только один параметр — (nonnull NSNumber*) reactTag . Эта экспортированная функция найдёт определённое представление с помощью addUIBlock, содержащего параметр viewRegistry, и вернёт компонент, основанный на reactTag, что позволит вызвать метод в правильном компоненте.
Стили
Поскольку все наши нативные представления React являются подклассами UIView, большинство атрибутов стиля будут работать так, как ожидается. Однако некоторые компоненты будут нуждаться в стиле по умолчанию, например UIDatePicker, который имеет фиксированный размер. Этот стиль по умолчанию важен для работы алгоритма макета, но мы также хотим иметь возможность переопределять стиль по умолчанию при использовании компонента. DatePickerIOS делает это, обернув нативный компонент в дополнительное представление с гибкой стилизацией и используя фиксированный стиль (который генерируется с константами, передаваемыми из нативного кода) в нативном внутреннем компоненте:
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 экспортируются из нативного кода путём получения фактической рамки нативного компонента следующим образом:
- (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),
}
};
}
Это руководство охватывает многие аспекты связи с пользовательскими нативными компонентами, но есть ещё много вещей, которые вам могут потребоваться рассмотреть, такие как пользовательские хуки для вставки и компоновки дочерних представлений. Если вы хотите углубиться ещё больше, ознакомьтесь с исходным кодом некоторых реализованных компонентов.
© 2022 Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://reactnative.dev/docs/native-components-ios