Documentación
Código específico de plataforma
Un mismo bundle de NSH se ejecuta en iOS, Android, macOS, Windows y Linux (y en Mac Catalyst), y en los navegadores mediante el web runtime. La mayor parte de una app no necesita ninguna comprobación de plataforma: los elementos, los estilos, el layout y los módulos se comportan igual en todas partes, y los componentes que construyes con ellos se ven igual en todas partes. Cuando algo realmente es específico de una plataforma, NSH le da un lugar claro, de modo que el código de plataforma sea visible y quede acotado:
| Rust | TSX | Para |
|---|---|---|
Métodos builder (.ripple(..), .input_accessory(..)) |
ios={{…}}, android={{…}}, desktop={{…}}, web={{…}} |
Props que solo tiene una plataforma. |
on_back_press, nsh_core::element::desktop |
nsh/mobile, nsh/desktop |
APIs que solo tienen las plataformas móviles o solo los escritorios. |
os(), idiom(), is_desktop() |
platform.os, platform.desktop, platform.select |
Elegir valores o código según la plataforma. |
.when(os() == Os::Ios, ..) |
clases ios: / android: / web: |
Estilos por plataforma. |
#Escritorio y móvil
Toda plataforma es de escritorio o móvil, según una única regla. Escritorio es macOS, Windows, Linux y los navegadores en el idioma de escritorio (un ratón y una ventana ancha, la web). Móvil es iOS, Android y los navegadores en el idioma de teléfono o tableta; Mac Catalyst ejecuta el backend de iOS, así que también es móvil, con props ios={{…}}. Las props desktop={{…}}, platform.select({ desktop, mobile }), platform.desktop y platform.idiom === "desktop" siguen todas esta regla, igual que los componentes de nsh/base (un menú es un popup en escritorio y una hoja en móvil) y, en Rust, is_desktop().
El isAvailable de nsh/desktop responde a una pregunta más acotada: si las APIs de ventana y menú de ese módulo funcionan, lo cual ocurre en macOS, Windows y Linux. Condiciona el layout, la densidad y la entrada con platform.desktop, y las llamadas a Window y Menu con isAvailable.
#Props exclusivas de una plataforma
En Rust, una prop que solo tiene una plataforma es un método builder como cualquier otro (.ripple(..), .elevation(..) para Android, .input_accessory(..) para iOS, .window_drag(..) para los escritorios); las demás plataformas la ignoran. En TSX, las props que existen en una sola plataforma se agrupan en un objeto que lleva su nombre. Cada elemento acepta ios, android, desktop (macOS, Windows, Linux y los navegadores en el idioma de escritorio) y web (navegadores); en las demás plataformas el objeto se ignora, y en su propia plataforma solo se aplican sus claves. Un navegador de escritorio aplica tanto desktop como web.
use nsh_base::dialog;
use nsh_core::{
Ripple,
element::{AutoFill, Orientation}, prelude::*,
};
view()
.on_press(open)
.active(|s| s.opacity(0.6))
.ripple(Ripple {
color: Some(rgba(0x0000001f).into()),
..Ripple::default()
})
.elevation(px(4.0))
.p_4()
.rounded(px(12.0))
.bg(white())
.child(text("Open"))
input().placeholder("Code").auto_fill(AutoFill::OneTimeCode)
when(shown, || {
dialog()
.supported_orientations([Orientation::Portrait])
.status_bar_translucent(true)
.child(…)
})<View
onPress={open}
activeStyle={{ opacity: 0.6 }}
android={{ ripple: { color: "rgba(0,0,0,0.12)" }, elevation: 4 }}
style={{ padding: 16, borderRadius: 12, backgroundColor: "#ffffff" }}
>
<Text>Open</Text>
</View>
<Input
placeholder="Code"
ios={{ textContentType: "oneTimeCode" }}
/>
<Dialog open={open()} ios={{ supportedOrientations: ["portrait"] }} android={{ statusBarTranslucent: true }}>
…
</Dialog>Los objetos de TSX son reactivos como cualquier prop; una clave que se elimina del objeto se borra.
Lo que existe hoy:
| Rust | TSX | En | Descripción |
|---|---|---|---|
.ripple(Ripple { .. }) |
android={{ ripple }} |
todos los elementos | El ripple de material mientras se pulsa: color, borderless, radius, foreground. |
.elevation(..) |
android={{ elevation }} |
todos los elementos | La sombra de material, en puntos, a partir de la forma de la vista. |
android={{ accessibilityLiveRegion }} |
todos los elementos | "none", "polite" o "assertive": TalkBack anuncia los cambios de la vista. |
|
.status_bar_translucent(..) |
android={{ statusBarTranslucent }} |
Dialog |
El contenido del diálogo llega por debajo de la barra de estado. |
.input_accessory(..) |
ios={{ inputAccessoryViewID }} |
Input |
El InputAccessoryView que se muestra sobre el teclado. |
.auto_fill(..) |
ios={{ textContentType }} |
Input |
Los nombres de iOS para los tipos de autofill (el AutoFill de Rust cubre ambas plataformas). |
.supported_orientations(..) |
ios={{ supportedOrientations }} |
Dialog |
Las orientaciones que permite. |
.back_forward_gestures(..) |
ios={{ allowsBackForwardNavigationGestures }} |
WebView |
Deslizar hacia atrás y hacia adelante en el historial de páginas. |
.header_right_system_item(..) |
ios={{ headerRightSystemItem }} |
NavPage |
El botón de barra propio de UIKit al final de la cabecera, que al pulsarlo ejecuta headerRight. |
.header_left_system_item(..) |
ios={{ headerLeftSystemItem }} |
NavPage |
El botón de barra propio de UIKit al inicio de la cabecera, que al pulsarlo ejecuta headerLeft. |
.header_right_symbol(..) |
ios={{ headerRightSymbol }} |
NavPage |
El botón de barra propio de UIKit que muestra un SF Symbol al final de la cabecera, que al pulsarlo ejecuta headerRight. |
.header_left_symbol(..) |
ios={{ headerLeftSymbol }} |
NavPage |
El botón de barra propio de UIKit que muestra un SF Symbol al inicio de la cabecera, que al pulsarlo ejecuta headerLeft. |
.back_forward_gestures(..) |
desktop={{ allowsBackForwardNavigationGestures }} |
WebView |
macOS: los deslizamientos de dos dedos en el trackpad avanzan y retroceden en el historial de páginas; Windows: deslizamientos en pantalla táctil; Linux: deslizamientos en el touchpad. |
.window_drag(..) |
desktop={{ windowDrag }} |
todos los elementos | Al pulsarlo arrastra la ventana, como lo haría la barra de título del sistema (consulta nsh/desktop). |
En TSX, desktop={{…}} y web={{…}} se aceptan en todas partes. Los tipos (IOSProps, AndroidProps, DesktopProps y sus formas por elemento) indican a tu editor qué acepta cada elemento.
Lo que un elemento significa en todas las plataformas es una prop de nivel superior (en Rust, un método con el mismo nombre), que se aplica allí donde tiene sentido: title, headerStyle, headerLargeTitle y headerBackTitle de NavPage configuran la cabecera del sistema de iOS y Android, su slot header es la cabecera de los escritorios y la web, y presentation de Dialog elige las hojas de iOS. closeOnBackdropPress, pagingEnabled, refreshing, hitSlop y el resto de AlertDialog.show funcionan en todas las plataformas. Una función propia del sistema de una plataforma va en su objeto y, cuando varias plataformas la tienen, en el de cada una con el mismo nombre: allowsBackForwardNavigationGestures de WebView, los deslizamientos del sistema por el historial de páginas, está en ios={{…}} y desktop={{…}}.
#Módulos de plataforma
En Rust, las APIs que solo tiene una plataforma son funciones de nsh_core::prelude (on_back_press, input_accessory_view) y de nsh_core::element::desktop (la ventana: set_title, set_title_bar, title_bar, minimize, toggle_maximize, toggle_full_screen, set_frame, set_level, order_front, close_window; menús: set_application_menu, popup_menu); en el resto de plataformas no hacen nada. En TSX, nsh/mobile contiene las APIs de iOS y Android (algunas de una sola plataforma) y nsh/desktop las de los escritorios. Ambos se pueden importar en todas las plataformas, de modo que un solo bundle se ejecuta en todas partes: en las demás plataformas sus funciones retornan de inmediato y sus componentes no ocupan espacio. Cada uno exporta isAvailable, que es true en sus propias plataformas; platform.os permite distinguirlos.
#nsh/mobile
isAvailable es true en iOS (con Mac Catalyst) y Android. Ambas plataformas tienen TabBar, la barra de pestañas del sistema.
#BackHandler (Android)
| Rust | TSX | Descripción |
|---|---|---|
on_back_press(listener) |
BackHandler.on("press", listener) |
Escucha el botón Atrás después de los diálogos y antes de las pilas; devuelve true cuando lo has gestionado. El listener desaparece con el componente (o efecto) que lo añadió; en TSX la llamada también devuelve una función que lo detiene antes. |
BackHandler.exitApp() |
Sale de la app. |
use nsh_core::prelude::*;
fn player() -> impl IntoElement {
let full_screen = signal(false);
// Back leaves full screen before it leaves the page. The listener goes
// with the view.
on_back_press(move || {
if !full_screen.get_untracked() {
return false;
}
full_screen.set(false);
true
});
// …
}import { BackHandler } from "nsh/mobile";
function Player() {
// Back leaves full screen before it leaves the page. The listener goes
// with the component.
BackHandler.on("press", () => {
if (!fullScreen()) return false;
fullScreen.set(false);
return true;
});
// …
}El botón Atrás lo gestiona primero el diálogo superior, después los listeners de retroceso (el más reciente primero), luego la pila de navegación superior, y la app se cierra cuando nadie lo ha gestionado. Consulta Navigation.
#InputAccessoryView (iOS)
| Rust | TSX | Descripción |
|---|---|---|
input_accessory_view(native_id) |
InputAccessoryView |
Una barra sobre el teclado de los inputs que nombran su id nativo (.input_accessory(..), ios={{ inputAccessoryViewID }}). |
Consulta Forms and the keyboard.
#nsh/desktop
Las apps de Rust dibujan sus propias barras de título con nsh_core::element::desktop (Building with Rust). El módulo de TSX:
| Exportación | Descripción |
|---|---|
Window |
La ventana: setTitle, setTitleBar({ style: "hidden" }) para una barra de título propia de la app, minimize, toggleMaximize, toggleFullScreen, close. |
titleBar |
La altura de la barra de título oculta y el espacio que ocupan los controles de ventana del sistema (reactivo). |
Menu |
setApplicationMenu(items) (la barra de menús, con atajos) y popup(items, at?) (menús contextuales). |
isAvailable |
true donde funcionan las APIs de ventana y menú: macOS, Windows y Linux. Los navegadores de escritorio tienen platform.desktop y conservan la ventana y los menús del navegador. |
Consulta nsh/desktop.
#La barra de estado
En ambas plataformas la barra de estado pertenece a la página o al diálogo que está encima, como en UIKit, donde decide el view controller superior: NavPage y Dialog aceptan statusBar (en Rust .status_bar(StatusBarStyle::Light), o un StatusBarSetting para los demás campos) con foreground ("auto", "light" para fondos oscuros, "dark" para fondos claros), hidden y animated. Gana la página o el diálogo mostrado más recientemente, campo por campo, y cuando desaparece vuelve a aplicarse el que está debajo. "statusBar" de nsh.json es el valor predeterminado de la app por debajo de ellos.
use nsh_base::nav_page;
use nsh_core::prelude::*;
fn photo_viewer() -> impl IntoElement {
nav_page()
.header_visible(false)
.status_bar(StatusBarStyle::Light)
.child(view().flex_1().bg(black()))
}import { View } from "nsh";
import { NavPage } from "nsh/base";
function PhotoViewer() {
return (
<NavPage headerVisible={false} statusBar={{ foreground: "light" }}>
<View style={{ flex: 1, backgroundColor: "#000000" }}>…</View>
</NavPage>
);
}En una app incrustada en una app nativa, el view controller de la app anfitriona decide la barra de estado en iOS. Una app que declara "capabilities" en nsh.json necesita "statusBar": true para cambiarla.
#platform.os and platform.select
os() en Rust y la constante platform de nsh en TSX indican dónde se ejecuta la app: Os::Ios, Os::Android, Os::MacOs, Os::Windows, Os::Linux o Os::Web, y platform.os "ios", "android", "macos", "windows", "linux" o "web" (Mac Catalyst es "ios", con platform.catalyst en cualquiera de sus dos idiomas; el backend de macOS es "macos", el backend de Linux "linux" y el backend de Windows "windows", y sus elementos aplican sus props desktop; los navegadores son "web", y también aplican las props desktop cuando platform.desktop es true). Consulta Device and environment para la versión, el idioma y el resto.
use nsh_core::{element::Os, prelude::*};
if os() == Os::Android {
// …
}import { platform } from "nsh";
if (platform.os === "android") {
// …
}#platform.select
En Rust, un match os() elige el valor, con is_desktop() para los escritorios. En TSX, platform.select(spec) devuelve el valor de esta plataforma. Busca la clave propia del SO, luego desktop o mobile según la regla anterior (platform.desktop), luego native (todas las plataformas salvo la web) y por último default:
use nsh_core::{element::Os, prelude::*};
fn card() -> View {
let card = view().rounded(px(12.0)).bg(white());
match os() {
Os::Ios => card.shadow_md(),
Os::Android => card.border_1().border_color(rgb(0xe5e7eb)),
_ => card,
}
}
let hint = match os() {
Os::Ios => "Swipe down to close",
Os::Android => "Press back to close",
_ => "Close",
};const styles = {
card: {
borderRadius: 12,
backgroundColor: "#ffffff",
...platform.select({
ios: { boxShadow: "0px 2px 8px rgb(0 0 0 / 0.12)" },
android: { borderWidth: 1, borderColor: "#e5e7eb" },
}),
},
};
const hint = platform.select({ ios: "Swipe down to close", android: "Press back to close", default: "Close" });Devuelve undefined cuando nada coincide y no hay default.
#Renderizar componentes distintos
Elige los componentes de la misma manera, a nivel de módulo (TSX) cuando la elección nunca cambia:
fn spinner() -> AnyElement {
if os() == Os::Ios {
ios_spinner().into_element()
} else {
material_spinner().into_element()
}
}const Spinner = platform.select({ ios: IosSpinner, default: MaterialSpinner })!;o en línea con when / <If>:
when(os() == Os::Ios, || input_accessory_view("bar").child(…))
.otherwise(|| text("Android has no input accessories."))<If when={platform.os === "ios"} else={<Text>Android has no input accessories.</Text>}>
<InputAccessoryView nativeID="bar">…</InputAccessoryView>
</If>#Variantes de plataforma en Tailwind
Rust aplica estilos por plataforma con .when(os() == Os::Ios, |v| ..); en TSX, las variantes ios:, android: y web: aplican clases en una sola plataforma:
view()
.rounded_xl()
.bg(white())
.p_4()
.when(os() == Os::Ios, |v| v.shadow_md())
.when(os() == Os::Android, |v| v.border_1().border_color(rgb(0xe4e4e7)))<View class="rounded-xl bg-white p-4 ios:shadow-md android:border android:border-zinc-200">…</View>
<Text class="text-base android:font-medium">Title</Text>#Preferir el comportamiento compartido
Antes de recurrir a una comprobación de plataforma, considera si la diferencia importa a tus usuarios. Los elementos y los estilos ya se corresponden con el comportamiento nativo de cada plataforma donde debe diferir (física del scroll, renderizado de texto, teclados, accesibilidad), y se mantienen iguales donde los usuarios esperan coherencia (el aspecto propio de tu app). Lo mejor es reservar las comprobaciones de plataforma para las convenciones en las que los usuarios confían: el botón Atrás y los toasts de Android, las hojas y los títulos grandes de iOS, los nombres de autofill, el ripple de Android.
#Relacionado
InputAccessoryViewyBackHandler: las referencias.- La API de dispositivo de
nsh:platform. - Native components: uso de las vistas que tu app anfitriona proporciona en cada plataforma.
- Tailwind: todas las variantes.