Tworzenie komponentu <InputAccessoryView> dla React Native
Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →
Motywacja
Trzy lata temu zgłoszono problem na GitHubie dotyczący dodania obsługi widżetu pomocniczego klawiatury w React Native.
Przez kolejne lata pojawiały się liczne "+1", różne obejścia i zero konkretnych zmian w RN w tej sprawie - aż do dziś. Zaczynając od iOS, udostępniamy API do obsługi natywnego widżetu pomocniczego klawiatury i z przyjemnością dzielimy się szczegółami implementacji.
Kontekst
Czym dokładnie jest widżet pomocniczy klawiatury? Z dokumentacji Apple dowiadujemy się, że to niestandardowy widok kotwiczony u góry systemowej klawiatury, gdy obiekt staje się first responder. Każdy obiekt dziedziczący po UIResponder może przedefiniować właściwość .inputAccessoryView jako read-write i zarządzać niestandardowym widokiem. Infrastruktura responderów montuje widok i synchronizuje go z klawiaturą systemową. Gesty zamykające klawiaturę (przeciągnięcie lub dotknięcie) są automatycznie stosowane do widżetu pomocniczego. Pozwala to budować interfejsy z interaktywnym zamykaniem klawiatury - kluczową funkcją w aplikacjach takich jak iMessage czy WhatsApp.
Istnieją dwa główne zastosowania dla kotwiczenia widoku nad klawiaturą. Pierwsze to tworzenie paska narzędzi klawiatury, jak selektor tła w kreatorze Facebooka.
W tym scenariuszu klawiatura jest skupiona na polu tekstowym, a widżet pomocniczy dostarcza dodatkowej funkcjonalności kontekstowej. W aplikacji mapowej mogą to być sugestie adresów, a w edytorze tekstu - narzędzia formatowania.
Obiektem Objective-C UIResponder posiadającym <InputAccessoryView> w tym przypadku jest wyraźnie <TextInput> stający się first responderem, który pod maską staje się instancją UITextView lub UITextField.
Drugi typowy scenariusz to przyklejone pola tekstowe:
Tutaj pole tekstowe jest częścią samego widżetu pomocniczego. Rozwiązanie powszechnie stosowane w aplikacjach messengeringowych, gdzie wiadomość można komponować podczas przewijania historii.
Kto posiada <InputAccessoryView> w tym przykładzie? Czy może to być znowu UITextView/UITextField? Pole tekstowe jest WEWNĄTRZ widżetu pomocniczego - tworzy to zależność cykliczną. Rozwiązanie tego problemu to temat na osobny artykuł. Spoiler: właścicielem jest generyczna podklasa UIView, której ręcznie każemy zostać first responderem.
Projekt API
Znając już zastosowania <InputAccessoryView>, kolejnym krokiem było zaprojektowanie API uwzględniającego oba scenariusze i współpracującego z istniejącymi komponentami RN jak <TextInput>.
Dla pasków narzędzi klawiatury istotne były:
-
Możliwość umieszczenia dowolnej hierarchii widoków RN w
<InputAccessoryView> -
Zdolność tej odłączonej hierarchii do odbierania dotknięć i modyfikowania stanu aplikacji
-
Powiązanie
<InputAccessoryView>z konkretnym<TextInput> -
Możliwość współdzielenia jednego
<InputAccessoryView>między wieloma polami tekstowymi
Punkt 1 osiągnęliśmy za pomocą koncepcji zbliżonej do portali React. W tym podejściu "przenosimy" widoki RN do hierarchii UIView zarządzanej przez infrastrukturę responderów. Ponieważ widoki RN renderują się jako UIView, jest to proste - wystarczy nadpisać:
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
i przekierować wszystkie subview do nowej hierarchii UIView. Dla punktu 2 skonfigurowaliśmy nowy RCTTouchHandler dla <InputAccessoryView>. Aktualizacje stanu realizowane są standardowymi callbackami. Dla punktów 3-4 użyliśmy pola nativeID do lokalizacji widżetu w kodzie natywnym podczas tworzenia komponentu <TextInput>. Funkcja ta wykorzystuje właściwość .inputAccessoryView natywnego pola tekstowego, skutecznie łącząc <InputAccessoryView> z <TextInput> w ich implementacjach ObjC.
Obsługa przyklejonych pól tekstowych (scenariusz 2) dodaje dodatkowe ograniczenia. Ponieważ pole tekstowe jest dzieckiem widżetu pomocniczego, powiązanie przez nativeID nie wchodzi w grę. Zamiast tego ustawiamy .inputAccessoryView generycznej UIView (poza ekranem) na naszą natywną hierarchię <InputAccessoryView>. Ręczne uczynienie tej UIView first responderem powoduje zamontowanie widżetu przez infrastrukturę responderów - koncepcja dokładnie wyjaśniona we wspomnianym artykule.
Wyzwania
Proces tworzenia tego API nie był pozbawiony wyzwań. Oto napotkane problemy i ich rozwiązania.
Początkowy pomysł zakładał nasłuchiwanie zdarzeń UIKeyboardWill(Show/Hide/ChangeFrame) w NSNotificationCenter. Ten wzorzec stosowany jest w bibliotekach open-source'owych i wewnętrznie w aplikacji Facebooka. Niestety, zdarzenia UIKeyboardDidChangeFrame nie były wywoływane na czas, aby zaktualizować ramkę <InputAccessoryView> podczas przeciągania, a zmiany wysokości klawiatury nie były wychwytywane. Powodowało to błędy typu:
Na iPhone X klawiatura tekstowa i emoji mają różne wysokości. Większość aplikacji używających zdarzeń klawiatury musiała łatać ten błąd. Naszym rozwiązaniem było pełne wykorzystanie .inputAccessoryView, gdzie infrastruktura responderów automatycznie obsługuje takie aktualizacje.
Kolejny podchwytliwy błąd dotyczył omijania przycisku Home na iPhone X. Można pomyśleć: "Apple stworzył safeAreaLayoutGuide właśnie po to!". Byliśmy równie naiwni. Problem? Natywna implementacja <InputAccessoryView> nie ma okna do zakotwiczenia aż do momentu pojawienia się. Rozwiązaliśmy to nadpisując -(BOOL)becomeFirstResponder i wymuszając constraintsy w tym momencie. Ale pojawił się kolejny problem: 
Widżet omija przycisk Home, ale treść z obszaru unsafe staje się widoczna. Rozwiązanie znaleźliśmy w tym radarze. Owinęliśmy natywną hierarchię <InputAccessoryView> kontenerem ignorującym safeAreaLayoutGuide. Kontener zakrywa treść w unsafe area, podczas gdy <InputAccessoryView> pozostaje w bezpiecznej strefie.
Przykład użycia
Oto przykład przycisku resetującego stan <TextInput> na pasku narzędzi:
class TextInputAccessoryViewExample extends React.Component<
{},
*,
> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}
render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() =>
this.setState({text: 'Placeholder Text'})
}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}
Przykład przyklejonych pól tekstowych znajdziesz w repozytorium.
Kiedy będzie dostępne?
Pełna implementacja tej funkcjonalności jest dostępna w tym commicie. Komponent <InputAccessoryView> będzie dostępny w nadchodzącym wydaniu wersji v0.55.0.
Miłego pisania :)