Saltar al contenido principal
Versión: 0.77

Componentes de UI nativos para iOS

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

información

Native Module y Native Components son nuestras tecnologías estables utilizadas por la arquitectura heredada. Serán desaprobadas en el futuro cuando la Nueva Arquitectura sea estable. La Nueva Arquitectura utiliza Turbo Native Module y Fabric Native Components para lograr resultados similares.

Existen una gran cantidad de componentes de interfaz de usuario nativos listos para usarse en las aplicaciones más recientes: algunos forman parte de la plataforma, otros están disponibles como bibliotecas de terceros, y otros más podrían estar en uso en tu propio portafolio. React Native ya incluye varios de los componentes más críticos de la plataforma, como ScrollView y TextInput, pero no todos, y ciertamente no aquellos que podrías haber creado para una aplicación anterior. Afortunadamente, podemos encapsular estos componentes existentes para integrarlos perfectamente con tu aplicación de React Native.

Al igual que la guía de módulos nativos, esta también es una guía más avanzada que asume cierta familiaridad con la programación para iOS. Esta guía te mostrará cómo construir un componente de UI nativo, guiándote a través de la implementación de un subconjunto del componente MapView existente en la biblioteca principal de React Native.

Ejemplo de MapView para iOS

Supongamos que queremos agregar un mapa interactivo a nuestra aplicación. Podríamos usar MKMapView, solo necesitamos hacerlo utilizable desde JavaScript.

Las vistas nativas son creadas y manipuladas por subclases de RCTViewManager. Estas subclases son funcionalmente similares a los controladores de vista, pero esencialmente son singletons: el bridge crea solo una instancia de cada una. Exponen vistas nativas al RCTUIManager, que delega en ellas para establecer y actualizar las propiedades de las vistas según sea necesario. Los RCTViewManager también suelen ser los delegados de las vistas, enviando eventos de vuelta a JavaScript a través del bridge.

Para exponer una vista puedes:

  • Crear una subclase de RCTViewManager para administrar tu componente.

  • Agregar la macro marcadora RCT_EXPORT_MODULE().

  • Implementar el método -(UIView *)view.

RNTMapManager.m
#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
nota

No intentes establecer las propiedades frame o backgroundColor en la instancia de UIView que expones a través del método -view. React Native sobrescribirá los valores establecidos por tu clase personalizada para que coincidan con las props de diseño de tu componente en JavaScript. Si necesitas este nivel de control, sería mejor envolver la instancia de UIView que deseas estilizar dentro de otra UIView y devolver la UIView contenedora en su lugar. Consulta el Issue 2948 para más contexto.

información

En el ejemplo anterior, prefijamos nuestro nombre de clase con RNT. Los prefijos se usan para evitar colisiones de nombres con otros frameworks. Los frameworks de Apple usan prefijos de dos letras, y React Native usa RCT como prefijo. Para evitar colisiones de nombres, recomendamos usar un prefijo de tres letras diferente a RCT en tus propias clases.

Luego necesitas un poco de JavaScript para convertir esto en un componente React utilizable:

MapView.tsx
import {requireNativeComponent} from 'react-native';

export default requireNativeComponent('RNTMap');

La función requireNativeComponent resuelve automáticamente RNTMap a RNTMapManager y exporta nuestra vista nativa para usarla en JavaScript.

MyApp.tsx
import MapView from './MapView.tsx';

export default function MyApp() {
return <MapView style={{flex: 1}} />;
}
nota

Al renderizar, no olvides estirar la vista, de lo contrario verás una pantalla en blanco.

Ahora tenemos un componente nativo de vista de mapa completamente funcional en JavaScript, con soporte para gestos nativos como zoom con pellizco. Sin embargo, aún no podemos controlarlo desde JavaScript.

Propiedades

Lo primero que podemos hacer para que este componente sea más utilizable es puentear algunas propiedades nativas. Digamos que queremos poder desactivar el zoom y especificar la región visible. Desactivar el zoom es un booleano, así que agregamos esta línea:

RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

Observa que especificamos explícitamente el tipo como BOOL: React Native usa RCTConvert internamente para convertir diversos tipos de datos al comunicarse a través del bridge, y los valores incorrectos mostrarán convenientes errores "RedBox" para notificarte un problema lo antes posible. Cuando las cosas son sencillas como esta, esta macro se encarga de toda la implementación por ti.

Para desactivar realmente el zoom, establecemos la propiedad en JavaScript:

MyApp.tsx
import MapView from './MapView.tsx';

export default function MyApp() {
return <MapView zoomEnabled={false} style={{flex: 1}} />;
}

Para documentar las propiedades (y los valores que aceptan) de nuestro componente MapView, agregaremos un componente wrapper y documentaremos la interfaz con TypeScript:

MapView.tsx
import {requireNativeComponent} from 'react-native';

const RNTMap = requireNativeComponent('RNTMap');

export default function MapView(props: {
/**
* Whether the user may use pinch gestures to zoom in and out.
*/
zoomEnabled?: boolean;
}) {
return <RNTMap {...props} />;
}

Ahora tenemos un componente wrapper bien documentado para trabajar.

A continuación, agreguemos la propiedad más compleja region. Comenzamos añadiendo el código nativo:

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

Esta es más compleja que el caso BOOL anterior. Ahora tenemos un tipo MKCoordinateRegion que requiere una función de conversión, y código personalizado para que la vista anime al establecer la región desde JS. Dentro del cuerpo de la función, json se refiere al valor bruto pasado desde JS. También tenemos una variable view que accede a la instancia de vista del manager, y defaultView que usamos para restablecer el valor predeterminado si JS envía un valor nulo.

Puedes escribir cualquier función de conversión para tu vista. Aquí está la implementación para MKCoordinateRegion mediante una categoría en RCTConvert, que utiliza la categoría existente de ReactNative RCTConvert+CoreLocation:

RNTMapManager.m
#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

Estas funciones de conversión están diseñadas para procesar de forma segura cualquier JSON desde JS, mostrando errores "RedBox" y devolviendo valores de inicialización estándar cuando se encuentran claves faltantes u otros errores de desarrollo.

Para completar el soporte de la propiedad region, la documentamos con TypeScript:

MapView.tsx
import {requireNativeComponent} from 'react-native';

const RNTMap = requireNativeComponent('RNTMap');

export default function MapView(props: {
/**
* The region to be displayed by the map.
*
* The region is defined by the center coordinates and the span of
* coordinates to display.
*/
region?: {
/**
* Coordinates for the center of the map.
*/
latitude: number;
longitude: number;

/**
* Distance between the minimum and the maximum latitude/longitude
* to be displayed.
*/
latitudeDelta: number;
longitudeDelta: number;
};
/**
* Whether the user may use pinch gestures to zoom in and out.
*/
zoomEnabled?: boolean;
}) {
return <RNTMap {...props} />;
}

Ahora podemos proporcionar la propiedad region a MapView:

MyApp.tsx
import MapView from './MapView.tsx';

export default function MyApp() {
const region = {
latitude: 37.48,
longitude: -122.16,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<MapView
region={region}
zoomEnabled={false}
style={{flex: 1}}
/>
);
}

Eventos

Tenemos un componente de mapa nativo controlable desde JS, pero ¿cómo manejamos eventos del usuario como zoom con pellizco o desplazamiento para cambiar la región visible?

Hasta ahora solo devolvimos una instancia MKMapView desde el método -(UIView *)view. Como no podemos agregar nuevas propiedades a MKMapView, creamos una subclase de MKMapView donde añadiremos un callback 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

Nota: Todos los RCTBubblingEventBlock deben tener el prefijo on. Declaramos una propiedad manejadora de eventos en RNTMapManager, la hacemos delegada de todas las vistas, y reenviamos eventos a JS llamando al bloque manejador desde la vista nativa.

RNTMapManager.m
#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

En el método delegado -mapView:regionDidChangeAnimated:, el bloque manejador de eventos se llama con los datos de región. Llamar al bloque manejador de eventos onRegionChange invoca el mismo callback prop en JavaScript, que típicamente procesamos en el componente wrapper para simplificar la API:

MapView.tsx
// ...

type RegionChangeEvent = {
nativeEvent: {
latitude: number;
longitude: number;
latitudeDelta: number;
longitudeDelta: number;
};
};

export default function MapView(props: {
// ...
/**
* Callback that is called continuously when the user is dragging the map.
*/
onRegionChange: (event: RegionChangeEvent) => unknown;
}) {
return <RNTMap {...props} onRegionChange={onRegionChange} />;
}
MyApp.tsx
import MapView from './MapView.tsx';

export default function MyApp() {
// ...

const onRegionChange = useCallback(event => {
const {region} = event.nativeEvent;
// Do something with `region.latitude`, etc.
});

return (
<MapView
// ...
onRegionChange={onRegionChange}
/>
);
}

Manejo de múltiples vistas nativas

Una vista de React Native puede tener múltiples vistas secundarias en el árbol, por ejemplo:

tsx
<View>
<MyNativeView />
<MyNativeView />
<Button />
</View>

En este ejemplo, la clase MyNativeView es un wrapper para NativeComponent que expone métodos, los cuales serán llamados en la plataforma iOS. MyNativeView está definido en MyNativeView.ios.js y contiene métodos proxy de NativeComponent.

Cuando el usuario interactúa con el componente, por ejemplo, haciendo clic en un botón, el backgroundColor de MyNativeView cambia. En este caso UIManager no sabría qué MyNativeView debe manejarse y cuál debe cambiar backgroundColor. A continuación encontrará la solución:

tsx
<View>
<MyNativeView ref={this.myNativeReference} />
<MyNativeView ref={this.myNativeReference2} />
<Button
onPress={() => {
this.myNativeReference.callNativeMethod();
}}
/>
</View>

Ahora el componente tiene una referencia a un MyNativeView particular que nos permite usar una instancia específica de MyNativeView. El botón puede controlar qué MyNativeView cambia su backgroundColor. Asumamos que callNativeMethod modifica el backgroundColor.

MyNativeView.ios.tsx
class MyNativeView extends React.Component {
callNativeMethod = () => {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.getViewManagerConfig('RNCMyNativeView').Commands
.callNativeMethod,
[],
);
};

render() {
return <NativeComponent ref={NATIVE_COMPONENT_REF} />;
}
}

callNativeMethod es nuestro método iOS personalizado que, por ejemplo, cambia el backgroundColor que se expone a través de MyNativeView. Este método usa UIManager.dispatchViewManagerCommand con tres parámetros:

  • (nonnull NSNumber \*)reactTag  -  ID de la vista React

  • commandID:(NSInteger)commandID  -  ID del método nativo a llamar

  • commandArgs:(NSArray<id> \*)commandArgs  -  Argumentos del método nativo que podemos pasar desde JS a nativo.

RNCMyNativeViewManager.m
#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];
}];

}

Aquí, callNativeMethod está definido en el archivo RNCMyNativeViewManager.m y contiene un solo parámetro: (nonnull NSNumber*) reactTag. Esta función exportada encontrará una vista específica usando addUIBlock, que incluye el parámetro viewRegistry y devuelve el componente basado en reactTag, permitiendo llamar al método en el componente correcto.

Estilos

Dado que todas nuestras vistas nativas de React son subclases de UIView, la mayoría de atributos de estilo funcionarán como se espera de forma inmediata. Algunos componentes necesitarán un estilo predeterminado, como UIDatePicker que tiene un tamaño fijo. Este estilo base es crucial para que el algoritmo de diseño funcione correctamente, pero también queremos poder sobrescribirlo al usar el componente. DatePickerIOS logra esto envolviendo el componente nativo en una vista adicional con estilos flexibles, mientras aplica un estilo fijo (generado con constantes provenientes de nativo) al componente interno:

DatePickerIOS.ios.tsx
import {UIManager} from 'react-native';
const RCTDatePickerIOSConsts = UIManager.RCTDatePicker.Constants;
...
render: function() {
return (
<View style={this.props.style}>
<RCTDatePickerIOS
ref={DATEPICKER}
style={styles.rkDatePickerIOS}
...
/>
</View>
);
}
});

const styles = StyleSheet.create({
rkDatePickerIOS: {
height: RCTDatePickerIOSConsts.ComponentHeight,
width: RCTDatePickerIOSConsts.ComponentWidth,
},
});

Las constantes RCTDatePickerIOSConsts se exportan desde nativo capturando el frame real del componente nativo así:

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

Esta guía cubrió muchos aspectos de la conexión de componentes nativos personalizados, pero hay más elementos que podrías necesitar considerar, como hooks personalizados para insertar y diseñar subvistas. Si deseas profundizar, explora el código fuente de algunos componentes implementados.