Natywne komponenty interfejsu użytkownika w iOS
Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →
Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →
Native Module i Native Components to nasze stabilne technologie używane w starszej architekturze. Zostaną one wycofane w przyszłości, gdy Nowa Architektura stanie się stabilna. Nowa Architektura wykorzystuje Turbo Native Module i Fabric Native Components, aby osiągnąć podobne rezultaty.
Istnieje mnóstwo natywnych widżetów UI gotowych do użycia w nowoczesnych aplikacjach - część z nich jest elementem platformy, inne dostępne są jako biblioteki stron trzecich, a jeszcze inne mogą być używane w twoim własnym portfolio. React Native zawiera opakowane wersje kilku kluczowych komponentów platformy, takich jak ScrollView czy TextInput, ale nie wszystkich i z pewnością nie tych, które mogłeś stworzyć samodzielnie do poprzedniej aplikacji. Na szczęście możemy opakować te istniejące komponenty dla płynnej integracji z twoją aplikacją React Native.
Podobnie jak przewodnik po modułach natywnych, to także zaawansowany przewodnik zakładający, że masz pewną znajomość programowania w iOS. Pokażemy ci, jak zbudować natywny komponent interfejsu użytkownika, przeprowadzając cię przez implementację uproszczonej wersji istniejącego komponentu MapView dostępnego w głównej bibliotece React Native.
Przykład MapView w iOS
Powiedzmy, że chcemy dodać interaktywną Mapę do naszej aplikacji - możemy użyć MKMapView, musimy tylko umożliwić jej używanie z JavaScriptu.
Natywne widoki są tworzone i zarządzane przez podklasy RCTViewManager. Te podklasy pełnią funkcję podobną do kontrolerów widoku, ale są w zasadzie singletonami - mostek tworzy tylko jedną instancję każdego. Udostępniają natywne widoki dla RCTUIManager, który deleguje do nich ustawianie i aktualizowanie właściwości widoków w razie potrzeby. RCTViewManager są również typowo delegatami dla widoków, wysyłając zdarzenia z powrotem do JavaScriptu przez mostek.
Aby udostępnić widok możesz:
-
Utworzyć podklasę
RCTViewManagerjako menedżer twojego komponentu. -
Dodać makro znacznika
RCT_EXPORT_MODULE(). -
Zaimplementować metodę
-(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
Nie próbuj ustawiać właściwości frame ani backgroundColor na instancji UIView, którą udostępniasz przez metodę -view.
React Native nadpisze wartości ustawione przez twoją klasę, aby dopasować je do właściwości układu twojego komponentu JavaScript.
Jeśli potrzebujesz tak szczegółowej kontroli, lepiej opakuj instancję UIView, którą chcesz ostylować, w inną UIView i zwróć tę opakowaną UIView.
Więcej kontekstu znajdziesz w Issue 2948.
W powyższym przykładzie dodaliśmy przedrostek RNT do nazwy klasy. Przedrostki służą do unikania konfliktów nazw z innymi frameworkami.
Frameworki Apple używają dwuliterowych przedrostków, a React Native używa RCT. Aby uniknąć konfliktów, zalecamy używanie w swoich klasach trzy literowego przedrostka innego niż RCT.
Następnie potrzebujesz trochę JavaScriptu, aby utworzyć z tego użyteczny komponent React:
import {requireNativeComponent} from 'react-native';
export default requireNativeComponent('RNTMap');
Funkcja requireNativeComponent automatycznie rozpoznaje RNTMap jako RNTMapManager i eksportuje nasz natywny widok do użycia w JavaScript.
import MapView from './MapView.tsx';
export default function MyApp() {
return <MapView style={{flex: 1}} />;
}
Podczas renderowania nie zapomnij rozciągnąć widoku, inaczej zobaczysz pusty ekran.
To teraz w pełni funkcjonalny natywny komponent mapy w JavaScript, z pełnym wsparciem dla gestów takich jak pinch-zoom. Nie możemy jeszcze jednak nim sterować z poziomu JavaScript.
Zatrzymuje działającą animację i resetuje wartość do oryginalnej.
Pierwszą rzeczą, którą możemy zrobić, aby zwiększyć użyteczność komponentu, jest przekazanie niektórych natywnych właściwości. Powiedzmy, że chcemy móc wyłączyć powiększanie i określić widoczny region. Wyłączenie powiększania to wartość boolowska, więc dodajemy tę jedną linię:
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
Zauważ, że jawnie określamy typ jako BOOL - React Native używa pod spodem RCTConvert do konwersji różnych typów danych podczas komunikacji przez mostek, a błędne wartości wyświetlą wygodne błędy "RedBox", aby jak najszybciej poinformować cię o problemie. Gdy sprawy są tak proste jak ta, cała implementacja jest obsługiwana za ciebie przez to makro.
Aby faktycznie wyłączyć powiększanie, ustawiamy właściwość w JavaScript:
import MapView from './MapView.tsx';
export default function MyApp() {
return <MapView zoomEnabled={false} style={{flex: 1}} />;
}
Aby udokumentować właściwości (i akceptowane przez nie wartości) naszego komponentu MapView, dodamy komponent opakowujący i udokumentujemy interfejs za pomocą TypeScript:
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} />;
}
Teraz mamy porządnie udokumentowany komponent opakowujący gotowy do użycia.
Następnie dodajmy bardziej złożoną właściwość region:
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
To jest bardziej skomplikowane niż poprzedni przypadek z BOOL. Mamy teraz typ MKCoordinateRegion, który wymaga funkcji konwersji, oraz niestandardowy kod zapewniający animację widoku przy ustawianiu regionu z JS. Wewnątrz dostarczonej funkcji json odnosi się do surowej wartości przekazanej z JS. Dostępna jest również zmienna view dająca dostęp do instancji widoku menedżera oraz defaultView służąca do resetowania właściwości do wartości domyślnej, jeśli JS prześle wartość null.
Możesz napisać dowolną funkcję konwersji dla swojego widoku - oto implementacja dla MKCoordinateRegion poprzez kategorię rozszerzającą RCTConvert. Wykorzystuje istniejącą kategorię ReactNative RCTConvert+CoreLocation:
#import "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
Te funkcje konwersji są zaprojektowane, by bezpiecznie przetwarzać dowolny JSON z JS, wyświetlając błędy "RedBox" i zwracając standardowe wartości inicjalizacji przy braku kluczy lub innych błędach deweloperskich.
Aby dokończyć wsparcie dla właściwości region, możemy ją udokumentować w TypeScript:
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} />;
}
Teraz możemy przekazać właściwość region do MapView:
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}}
/>
);
}
Zdarzenia
Mamy więc natywny komponent mapy sterowany z JS, ale jak obsługiwać zdarzenia od użytkownika jak pinch-zoom czy przesuwanie zmieniające widoczny region?
Dotąd zwracaliśmy tylko instancję MKMapView z metody menedżera -(UIView *)view. Nie możemy dodawać nowych właściwości do MKMapView, więc tworzymy nową podklasę MKMapView dla naszego Widoku, gdzie dodajemy callback onRegionChange:
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>
@interface RNTMapView: MKMapView
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;
@end
#import "RNTMapView.h"
@implementation RNTMapView
@end
Wszystkie bloki RCTBubblingEventBlock muszą mieć prefiks on. Następnie deklarujemy właściwość obsługi zdarzeń w RNTMapManager, ustawiamy go jako delegata dla wszystkich widoków i przekazujemy zdarzenia do JS przez wywołanie bloku obsługi z widoku natywnego.
#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
W metodzie delegata -mapView:regionDidChangeAnimated: blok obsługi zdarzeń jest wywoływany na odpowiednim widoku z danymi regionu. Wywołanie bloku onRegionChange uruchamia ten sam callback w JavaScript. Ten callback otrzymuje surowe zdarzenie, które zwykle przetwarzamy w komponencie opakowującym dla uproszczenia API:
// ...
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} />;
}
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}
/>
);
}
Obsługa wielu widoków natywnych
Widok React Native może mieć wiele widoków potomnych w drzewie, np.
<View>
<MyNativeView />
<MyNativeView />
<Button />
</View>
W tym przykładzie klasa MyNativeView jest opakowaniem dla NativeComponent i udostępnia metody wywoływane na iOS. MyNativeView jest zdefiniowane w MyNativeView.ios.js i zawiera metody proxy NativeComponent.
Gdy użytkownik wchodzi w interakcję (np. klikając przycisk), zmienia się backgroundColor MyNativeView. W tym przypadku UIManager nie wie, którym MyNativeView zarządzać i który ma zmienić backgroundColor. Poniższe rozwiązanie:
<View>
<MyNativeView ref={this.myNativeReference} />
<MyNativeView ref={this.myNativeReference2} />
<Button
onPress={() => {
this.myNativeReference.callNativeMethod();
}}
/>
</View>
Komponent ma teraz referencję do konkretnego MyNativeView, co pozwala użyć określonej instancji MyNativeView. Przycisk może kontrolować, który MyNativeView zmieni backgroundColor. Zakładamy, że callNativeMethod zmienia 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 to nasza niestandardowa metoda iOS (np. zmieniająca backgroundColor dostępny przez MyNativeView). Wykorzystuje UIManager.dispatchViewManagerCommand wymagające 3 parametrów:
-
(nonnull NSNumber \*)reactTag- identyfikator widoku React -
commandID:(NSInteger)commandID- ID natywnej metody do wywołania -
commandArgs:(NSArray<id> \*)commandArgs- Argumenty metody natywnej, które możemy przekazać z JavaScript do kodu natywnego.
#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];
}];
}
W tym przypadku callNativeMethod jest zdefiniowana w pliku RNCMyNativeViewManager.m i przyjmuje tylko jeden parametr (nonnull NSNumber*) reactTag. Ta eksportowana funkcja znajdzie odpowiedni widok używając addUIBlock, który zawiera parametr viewRegistry i zwraca komponent na podstawie reactTag, umożliwiając wywołanie metody na właściwym komponencie.
Style
Ponieważ wszystkie nasze natywne widoki React są podklasami UIView, większość atrybutów stylu działa od razu zgodnie z oczekiwaniami. Niektóre komponenty wymagają jednak domyślnego stylu, jak np. UIDatePicker o stałym rozmiarze. Ten domyślny styl jest kluczowy dla poprawnego działania algorytmu układu, ale chcemy też móc nadpisać go podczas używania komponentu. DatePickerIOS osiąga to poprzez opakowanie natywnego komponentu w dodatkowy widok z elastycznymi stylami i zastosowanie stałego stylu (generowanego ze stałych przekazanych z natywnego) na wewnętrznym komponencie natywnym:
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,
},
});
Stałe RCTDatePickerIOSConsts są eksportowane z natywnego kodu poprzez pobranie rzeczywistej ramki natywnego komponentu w następujący sposób:
- (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),
}
};
}
Ten przewodnik omówił wiele aspektów mostkowania niestandardowych komponentów natywnych, ale istnieje jeszcze więcej do rozważenia, jak niestandardowe hooki do wstawiania i układania widoków potomnych. Jeśli chcesz zagłębić się bardziej, sprawdź kod źródłowy niektórych zaimplementowanych komponentów.