API
nsh/mobile
nsh/mobile contiene las capacidades específicas de iOS y Android: la barra de pestañas del sistema, TabBar, en ambas; InputAccessoryView, una barra sobre el teclado, en iOS; y BackHandler, el botón de retroceso, en Android. El código Rust usa nsh_core::mobile y nsh_core::prelude; TSX importa nsh/mobile. El módulo se puede importar en todas las plataformas, de modo que un mismo bundle funciona en todas partes: en las demás plataformas sus componentes no ocupan espacio y sus llamadas retornan de inmediato, y isAvailable indica si estás en iOS o Android (platform.os de nsh indica cuál). Lo que ofrecen los escritorios está en nsh/desktop.
use nsh_core::mobile::{TabBarItem, tab_bar};import { BackHandler, InputAccessoryView, isAvailable, TabBar } from "nsh/mobile";
import type { TabBarItem, TabBarProps } from "nsh/mobile";#Alcance
Este módulo expone capacidades específicas de las plataformas móviles, como la barra de pestañas del sistema para la navegación de nivel superior de la app. Los controles comunes pertenecen a nsh/base: su Switch, por ejemplo, comparte comportamiento y API en teléfonos, escritorios y la web, con la pista, el pulgar y los estilos de estado proporcionados por la app. TabBar sirve a la navegación de las apps móviles; Tabs sirve al contenido con pestañas en todas las plataformas. Aquí solo pertenecen las capacidades con semántica de interacción específica de móvil. Una apariencia, un tamaño o una animación del sistema, por sí solos, no cambian el rol compartido de un control común. Esta distinción guía qué capacidades pertenecen aquí (Objetivos y principios de diseño).
#isAvailable
const isAvailable: boolean;
true en iOS, el simulador de iOS, Mac Catalyst y Android; false en el resto. Rust:
matches!(os(), Os::Ios | Os::Android).
#TabBar
Una barra de pestañas cambia entre las secciones de nivel superior de una app: registros y estadísticas, bandeja de entrada y ajustes. TabBar es la barra propia del sistema, así que se ve y se comporta como las apps del sistema: UITabBar en iOS y iPadOS (Liquid Glass y su selección en iOS 26), la barra de navegación de Material 3 en Android, con los colores del sistema, en claro y oscuro. La app proporciona las pestañas (items), indica cuál está seleccionada (selectedIndex), recibe la elección del usuario (onChange), coloca la barra con su class y su style, y muestra ella misma el contenido de la sección seleccionada. Para pestañas dentro de una página, y en todas las plataformas, usa Tabs de nsh/base.
use nsh_core::{
mobile::{TabBarItem, tab_bar},
prelude::*,
};
fn sections() -> impl IntoElement {
let section = signal(0u32);
view()
.flex_1()
.child(text(move || match section.get() {
0 => "Records",
_ => "Stats",
}))
.child(
tab_bar()
.items(vec![
TabBarItem::new("Records").icon("list.bullet"),
TabBarItem::new("Stats").icon("chart.bar"),
])
.selected_index(section)
.on_change(move |index: &u32| section.set(*index))
.absolute()
.left_0()
.right_0()
.bottom_0(),
)
}import { mount, signal, Text, View } from "nsh";
import { TabBar } from "nsh/mobile";
import { platform } from "nsh";
function App() {
const section = signal(0);
return (
<View class="flex-1">
<Text>{section() === 0 ? "Records" : "Stats"}</Text>
<TabBar
items={[
{ title: "Records", icon: platform.select({ ios: "list.bullet", android: "ic_records" }) },
{ title: "Stats", icon: platform.select({ ios: "chart.bar", android: "ic_stats" }) },
]}
selectedIndex={section()}
onChange={({ index }) => section.set(index)}
class="absolute inset-x-0 bottom-0"
/>
</View>
);
}
mount(App);TabBar viene de nsh/mobile, y nsh_core::mobile en Rust. La selección pertenece a la app: un toque informa onChange con el índice de la pestaña ({ index }), la barra muestra la elección de inmediato, como la del sistema, y la app muestra esa sección y devuelve el índice como selectedIndex. Su altura es la del sistema cuando el estilo no la fija. En Rust, tab_bar() crea una barra, con un setter por prop (.items(..), .selected_index(..)), un listener por evento (.on_change(..)) y los métodos de estilo tipados; TabBarItem::new(title).icon(name) crea una pestaña.
#Propiedades
| Prop | Rust | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|---|
items |
.items(..) |
TabBarItem[] |
[] |
Las pestañas, en orden. |
selectedIndex |
.selected_index(..) |
number |
0 |
La pestaña seleccionada, por índice; un índice fuera del rango de pestañas no selecciona ninguna. |
hidden |
.hidden(..) |
boolean |
false |
iOS: oculta la barra sin transición. La app es dueña de este valor entre push y pop. La barra se muestra cuando tanto esta prop como la solicitud hidesBottomBarWhenPushed de la pila visible son falsas. |
class, style |
métodos de estilo | string, StyleProp<ViewStyle> |
ninguno | Dónde está la barra: su posición y tamaño. Su aspecto es el del sistema, con la pestaña seleccionada en el primary del tema. |
testID |
.test_id(..) |
string |
ninguno | Localiza la barra en las pruebas y en nsh test --trace. |
#TabBarItem
| Campo | Rust | Tipo | Descripción |
|---|---|---|---|
title |
TabBarItem::new(title) |
string |
El título de la pestaña, que leen los lectores de pantalla. |
icon |
.icon(name) |
string |
El icono de la pestaña, por el nombre que le da la plataforma: un SF Symbol en iOS ("house"), un drawable de la app en Android (res/drawable/home.xml es "home", "android:ic_menu_search" el del sistema). platform.select elige uno por plataforma. |
#Eventos
| Evento | Rust | Descripción |
|---|---|---|
onChange |
.on_change(|index: &u32| ..) |
El usuario eligió una pestaña: { index }, su índice. La barra muestra la elección de inmediato; la app muestra el contenido de la pestaña y establece selectedIndex. |
#Notas por plataforma
- iOS y Mac Catalyst:
UITabBar, con la apariencia del sistema para la versión de iOS: la barra flotante Liquid Glass y su selección en iOS 26. Los iconos son SF Symbols. Elprimarydel tema es eltintColorde la barra, el color de la pestaña seleccionada; las demás pestañas conservan el del sistema. - Android: la barra de navegación de Material 3, construida con las vistas de la plataforma: un icono en su indicador activo sobre cada etiqueta, una ondulación (ripple) al pulsar, los colores dinámicos del sistema en Android 12 y posteriores y los colores base de Material en versiones anteriores, en claro u oscuro según la apariencia de la app. En la parte inferior de la ventana se extiende bajo la barra de navegación, más alta en la medida de su inset, como la de Material. Los iconos son drawables de la app (teñidos por la barra). El icono y la etiqueta de la pestaña seleccionada toman el
primarydel tema, sobre un indicador activo de ese color al 12 % de opacidad; las demás pestañas conservan los del sistema. - Escritorios (macOS, Windows, Linux, GPUI) y la web: las secciones se muestran con Tabs, con el aspecto de la app.
TabBarno ocupa espacio allí, así que un único layout sirve para ambos casos, elegido medianteplatform.os(la API de dispositivo densh).
#InputAccessoryView
En iOS y Mac Catalyst; en Android es una vista vacía y oculta.
function InputAccessoryView(props: InputAccessoryViewProps): JSX.Element;
interface InputAccessoryViewProps {
nativeID: string;
style?: StyleProp<ViewStyle>;
class?: string;
children?: JSX.Element;
}
Una barra sobre el teclado (inputAccessoryView de UIKit), que se muestra mientras tiene el foco un input cuyo ios={{ inputAccessoryViewID }} coincide con su nativeID. En Rust: input_accessory_view(native_id), que se muestra para los inputs cuyo .input_accessory(native_id) coincide. Tiene el ancho de la ventana y la altura de su contenido, y se mueve con el teclado de forma nativa.
| Prop | Tipo | Rust | Descripción |
|---|---|---|---|
nativeID |
string |
input_accessory_view(native_id) |
Los inputs con este inputAccessoryViewID muestran la barra. |
style |
StyleProp<ViewStyle> |
Métodos de Styled (.bg(…)) |
El estilo de la barra (su fondo, por ejemplo). |
class |
string |
Clases de Tailwind. | |
children |
JSX.Element |
.child(…) |
El contenido de la barra. |
No renderiza nada en el lugar del árbol donde se coloca, y no renderiza nada en absoluto en Android, así que puedes colocarla junto al input al que pertenece.
use nsh_core::prelude::*;
#[component]
fn Note() -> impl IntoElement {
let note = signal(String::new());
let field = element_ref();
view()
.child(
input()
.element_ref(&field)
.value(note)
.on_change(move |value: &str| note.set(value.to_string()))
.placeholder("Write a note")
.input_accessory("note-bar"),
)
.child(
input_accessory_view("note-bar").bg(rgb(0xf1f5f9)).child(
view()
.flex_row()
.items_center()
.p(px(10.0))
.child(
text(move || format!("{} characters", note.with(|n| n.chars().count())))
.flex_1()
.text_color(rgb(0x64748b)),
)
.child(text("Done").font_weight(FontWeight::SEMIBOLD).on_press(move || field.blur())),
),
)
}import { type InputRef, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { InputAccessoryView } from "nsh/mobile";
function Note() {
const text = signal("");
let input: InputRef | undefined;
return (
<View>
<Input
ref={(n) => (input = n)}
value={text()}
onChange={({ value }) => text.set(value)}
placeholder="Write a note"
ios={{ inputAccessoryViewID: "note-bar" }}
/>
<InputAccessoryView nativeID="note-bar" style={{ backgroundColor: "#f1f5f9" }}>
<View style={{ flexDirection: "row", alignItems: "center", padding: 10 }}>
<Text style={{ flex: 1, color: "#64748b" }}>{text().length} characters</Text>
<Text onPress={() => input?.blur()} style={{ fontWeight: "600" }}>Done</Text>
</View>
</InputAccessoryView>
</View>
);
}Consulta también Formularios y teclado.
#BackHandler
En Android; en iOS sus listeners se conservan pero nunca se invocan, y exitApp() retorna de inmediato.
const BackHandler: {
on(event: "press", listener: () => boolean | void): () => void;
exitApp(): void;
};
El botón de retroceso del sistema y el gesto de retroceso. Los diálogos gestionan primero la pulsación de retroceso (un diálogo se cierra); después se ejecutan los listeners de BackHandler, empezando por el añadido más recientemente; cuando ninguno la gestiona, la pila de navegación superior hace pop, y si no hay página a la que volver, la app se cierra.
| Miembro | Rust | Descripción |
|---|---|---|
on("press", listener) |
on_back_press(listener) |
listener devuelve true cuando gestionó la pulsación. Devuelve una función que deja de escuchar; un listener añadido mientras se renderiza un componente (o se ejecuta un efecto) se detiene junto con él, tanto en Rust como en TSX. |
exitApp() |
el listener devuelve false |
Sale de la app. Un listener de Rust que devuelve false deja que la pulsación continúe, de modo que la pila retrocede o la app se cierra. |
use nsh_base::alert_dialog;
use nsh_core::prelude::*;
#[component]
fn Editor() -> impl IntoElement {
let dirty = signal(false);
on_back_press(move || {
if !dirty.get_untracked() {
return false; // let the app go back or exit
}
spawn(async move {
let answer = alert_dialog().title("Discard changes?").cancel("keep", "Keep").destructive("discard", "Discard").show().await;
if answer.is_ok_and(|r| r.action.as_deref() == Some("discard")) {
dirty.set(false); // the next back press leaves
}
});
true
});
// ...
}import { signal } from "nsh";
import { BackHandler } from "nsh/mobile";
import { AlertDialog } from "nsh/base";
function Editor() {
const dirty = signal(false);
// Stops listening when Editor goes.
BackHandler.on("press", () => {
if (!dirty()) return false; // let the app go back or exit
AlertDialog.show({
title: "Discard changes?",
actions: [{ label: "Keep", role: "cancel", id: "keep" }, { label: "Discard", role: "destructive", id: "discard" }],
}).then(({ action }) => action === "discard" && BackHandler.exitApp());
return true;
});
// ...
}La barra de estado se configura mediante el statusBar de las páginas y diálogos desde nsh/base, tanto en iOS como en Android.