Saltar al contenido principal

Construyendo <InputAccessoryView> para React Native

· 7 min de lectura
Peter Argany
Ingeniero de Software en Facebook
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 →

Motivación

Hace tres años, se abrió un issue en GitHub para solicitar compatibilidad con input accessory view en React Native.

Durante los años siguientes, hubo innumerables '+1', diversas soluciones alternativas y cero cambios concretos en RN sobre este tema, hasta hoy. Comenzando con iOS, exponemos una API para acceder a la vista de accesorios nativa y nos emociona compartir cómo la construimos.

Antecedentes

¿Qué es exactamente una input accessory view? Según la documentación para desarrolladores de Apple, es una vista personalizable que puede anclarse en la parte superior del teclado del sistema cuando un receptor se convierte en primer respondedor. Cualquier elemento que herede de UIResponder puede redeclarar la propiedad .inputAccessoryView como lectura-escritura y gestionar una vista personalizada aquí. La infraestructura de respondedores monta la vista y la mantiene sincronizada con el teclado del sistema. Los gestos que ocultan el teclado (como arrastrar o tocar) se aplican a la vista de accesorios a nivel de framework. Esto permite crear contenido con ocultamiento interactivo del teclado, una característica fundamental en aplicaciones de mensajería de primer nivel como iMessage y WhatsApp.

Existen dos casos de uso comunes para anclar una vista al teclado. El primero es crear una barra de herramientas, como el selector de fondos en el compositor de Facebook.

En este escenario, el teclado está enfocado en un campo de texto, y la vista de accesorios proporciona funcionalidad adicional contextual. En aplicaciones de mapas podría mostrar sugerencias de direcciones, mientras que en editores de texto podría incluir herramientas de formato avanzado.


El UIResponder de Objective-C que posee el <InputAccessoryView> en este caso es claro: el <TextInput> se convierte en primer respondedor, y en el código nativo esto se traduce en una instancia de UITextView o UITextField.

El segundo escenario común son entradas de texto persistentes:

Aquí, la entrada de texto forma parte de la propia vista de accesorios. Es común en aplicaciones de mensajería, donde puedes componer mensajes mientras navegas por un hilo de conversación.


¿Quién posee el <InputAccessoryView> aquí? ¿Puede ser nuevamente UITextView o UITextField? La entrada de texto está dentro de la vista de accesorios, lo que sugiere dependencia circular. Resolver esto merece su propia publicación. Spoiler: el propietario es una subclase genérica de UIView que invocamos manualmente con becomeFirstResponder.

Diseño de API

Ahora que sabemos qué es <InputAccessoryView> y cómo usarlo, el siguiente paso es diseñar una API coherente para ambos casos de uso, compatible con componentes existentes como <TextInput>.

Para barras de herramientas del teclado, debemos considerar:

  1. Poder integrar cualquier jerarquía de vistas de React Native en el <InputAccessoryView>.

  2. Que esta jerarquía de vistas genérica y desacoplada acepte interacciones táctiles y pueda manipular el estado de la aplicación.

  3. Vincular un <InputAccessoryView> a un <TextInput> específico.

  4. Compartir un <InputAccessoryView> entre múltiples entradas de texto sin duplicar código.

Podemos lograr el punto #1 usando un concepto similar a los portales de React. En este diseño, trasladamos las vistas de React Native a una jerarquía UIView gestionada por la infraestructura de respuesta. Dado que las vistas de React Native se renderizan como UIViews, esto es bastante directo: simplemente podemos sobrescribir:

- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex

y redirigir todas las subvistas a una nueva jerarquía UIView. Para el punto #2, configuramos un nuevo RCTTouchHandler para el <InputAccessoryView>. Las actualizaciones de estado se logran mediante callbacks de eventos regulares. Para los puntos #3 y #4, usamos el campo nativeID para localizar la jerarquía UIView de la vista accesoria en código nativo durante la creación de un componente <TextInput>. Esta función utiliza la propiedad .inputAccessoryView del campo de texto nativo subyacente. Esto enlaza efectivamente <InputAccessoryView> con <TextInput> en sus implementaciones de ObjC.

Soportar campos de texto persistentes (escenario 2) añade más restricciones. En este diseño, la vista accesoria tiene un campo de texto como hijo, por lo que vincular mediante nativeID no es viable. En su lugar, establecemos la propiedad .inputAccessoryView de una UIView genérica fuera de pantalla a nuestra jerarquía nativa <InputAccessoryView>. Al indicar manualmente a esta UIView genérica que se convierta en primera respondedora, la jerarquía se monta mediante la infraestructura de respuesta. Este concepto se explica detalladamente en la publicación de blog mencionada anteriormente.

Dificultades

Por supuesto, no todo fue sencillo al construir esta API. Estas son algunas dificultades que encontramos y cómo las solucionamos.

Una idea inicial para construir esta API implicaba escuchar a NSNotificationCenter los eventos UIKeyboardWill(Show/Hide/ChangeFrame). Este patrón se usa en bibliotecas de código abierto y en partes internas de la app de Facebook. Desafortunadamente, los eventos UIKeyboardDidChangeFrame no se activaban a tiempo para actualizar el marco de <InputAccessoryView> durante deslizamientos. Además, estos eventos no capturan cambios en la altura del teclado. Esto genera errores que se manifiestan así:

En iPhone X, los teclados de texto y emoji tienen alturas diferentes. Muchas apps que usan eventos de teclado para manipular campos de texto tuvieron que corregir este error. Nuestra solución fue comprometernos a usar la propiedad .inputAccessoryView, haciendo que la infraestructura de respuesta gestione estas actualizaciones de marco.


Otro error complejo fue evitar la barra de inicio en iPhone X. Podrías pensar: "¡Apple creó safeAreaLayoutGuide para esto, es trivial!". Fuimos igual de ingenuos. El primer problema es que la implementación nativa de <InputAccessoryView> no tiene ventana para anclarse hasta que está por aparecer. Podemos sobrescribir -(BOOL)becomeFirstResponder y aplicar restricciones de diseño allí. Al cumplir estas restricciones, la vista accesoria sube, pero aparece otro error:

La vista accesoria evita la barra de inicio, pero ahora se ve contenido tras el área insegura. La solución está en este radar. Envolví la jerarquía nativa de <InputAccessoryView> en un contenedor que no cumple las restricciones de safeAreaLayoutGuide. El contenedor nativo cubre el contenido del área insegura, mientras el <InputAccessoryView> permanece dentro de los límites seguros.


Ejemplo de Uso

Este ejemplo crea un botón en la barra de herramientas del teclado para resetear el estado de <TextInput>:

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

Otro ejemplo de Campos de Texto Persistentes está disponible en el repositorio.

¿Cuándo podré usar esto?

El commit completo para la implementación de esta característica está aquí. <InputAccessoryView> estará disponible en la próxima versión v0.55.0.

¡Feliz escritura con teclado :)