API

nsh/mobile

nsh/mobile 收录 iOS 和 Android 的专属能力:两者都有的系统标签栏 TabBar;iOS 的 InputAccessoryView,键盘上方的工具栏;Android 的 BackHandler,返回键。Rust 代码使用 nsh_core::mobile 和 nsh_core::prelude;TSX 导入 nsh/mobile。这个模块在所有平台上都可以导入,所以同一个 bundle 能在各处运行:在其他平台上,它的组件不占空间、调用立即返回,isAvailable 告诉你当前是否在 iOS 或 Android 上(nsh 的 platform.os 区分具体平台)。桌面的能力在 nsh/desktop 中。

use nsh_core::mobile::{TabBarItem, tab_bar};

#模块范围

这个模块提供移动平台专属的能力,例如用于应用顶层导航的系统标签栏。通用控件属于 nsh/base:例如 Switch 在手机、桌面和 Web 上共享行为与 API,轨道、滑块和各状态的样式由应用提供。TabBar 服务于移动应用导航,Tabs 服务于跨平台的标签内容切换。这个模块仅接受具有移动平台专属交互语义的能力。系统外观、尺寸或动画本身仍属于通用控件的共享职责。这个职责区别决定哪些能力适合放在这里(目标与设计原则)。

#isAvailable

const isAvailable: boolean;

在 iOS、iOS 模拟器、Mac Catalyst 和 Android 上为 true,其他平台为 false。Rust:matches!(os(), Os::Ios | Os::Android)。

#TabBar

标签栏在应用的几个顶层部分之间切换:记录和统计、收件箱和设置。TabBar 就是系统自己的标签栏,所以它的外观和行为与系统应用一致:iOS 和 iPadOS 上是 UITabBar(iOS 26 上是 Liquid Glass 及其选中效果),Android 上是 Material 3 的导航栏,使用系统的颜色,浅色和深色都一样。应用提供标签(items),指明选中哪一个(selectedIndex),接收用户的选择(onChange),用 class 和 style 摆放标签栏,并自己显示选中部分的内容。页面内的标签,以及在所有平台上,请使用 nsh/base 的 Tabs。

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(),
        )
}

TabBar 来自 nsh/mobile,在 Rust 中是 nsh_core::mobile。选中状态属于应用:一次点按通过 onChange 报告该标签的索引({ index }),标签栏像系统的一样立即显示这次选择,应用显示那一部分并把索引作为 selectedIndex 传回。样式没有给定高度时,高度是系统的。在 Rust 中,tab_bar() 创建它,每个 prop 一个 setter(.items(..)、.selected_index(..)),每个事件一个监听方法(.on_change(..)),以及带类型的样式方法;TabBarItem::new(title).icon(name) 创建一个标签。

#Props

PropRust类型默认值说明
items.items(..)TabBarItem[][]各个标签,按顺序。
selectedIndex.selected_index(..)number0选中的标签的索引;超出标签数量的索引不选中任何标签。
hidden.hidden(..)booleanfalseiOS:立即隐藏标签栏。应用在页面推入和弹出期间持有这个值;当它和当前可见导航栈的 hidesBottomBarWhenPushed 请求都为 false 时,标签栏显示。
class、style样式方法string、StyleProp<ViewStyle>无标签栏的位置和尺寸。外观是系统的,选中的标签使用主题的 primary。
testID.test_id(..)string无在测试和 nsh test --trace 中找到标签栏。

#TabBarItem

字段Rust类型说明
titleTabBarItem::new(title)string标签的标题,屏幕阅读器会读出它。
icon.icon(name)string标签的图标,用平台对它的称呼:iOS 上是 SF Symbol("house"),Android 上是应用的 drawable(res/drawable/home.xml 写作 "home","android:ic_menu_search" 是系统的)。用 platform.select 按平台选择。

#事件

事件Rust说明
onChange.on_change(|index: &u32| ..)用户选择了一个标签:{ index },它的索引。标签栏立即显示这次选择;应用显示该标签的内容并设置 selectedIndex。

#平台说明

  • iOS 和 Mac Catalyst:UITabBar,采用当前 iOS 版本的系统外观:iOS 26 上是悬浮的 Liquid Glass 标签栏及其选中效果。图标是 SF Symbols。主题的 primary 是标签栏的 tintColor,即选中标签的颜色;其他标签保持系统的颜色。
  • Android:Material 3 的导航栏,由平台的 View 组成:每个标签的文字上方是放在激活指示器中的图标,按压时有水波纹;Android 12 及以上使用系统的动态颜色,更早的版本使用 Material 的基准颜色;浅色或深色跟随应用的外观。位于窗口底部时,它延伸到导航栏下方,高度增加导航栏的 inset,与 Material 的一致。图标是应用的 drawable(由标签栏着色)。选中标签的图标和文字使用主题的 primary,激活指示器是这个颜色的 12% 不透明度;其他标签保持系统的颜色。
  • 桌面(macOS、Windows、Linux、GPUI)和 Web:各个部分用 Tabs 显示,外观由应用决定。TabBar 在这些平台上不占空间,所以一个布局可以同时包含两者,用 platform.os(nsh 的设备 API)来选择。

#InputAccessoryView

在 iOS 和 Mac Catalyst 上可用;在 Android 上是一个空的隐藏视图。

function InputAccessoryView(props: InputAccessoryViewProps): JSX.Element;

interface InputAccessoryViewProps {
  nativeID: string;
  style?: StyleProp<ViewStyle>;
  class?: string;
  children?: JSX.Element;
}

键盘上方的工具栏(UIKit 的 inputAccessoryView),当 ios={{ inputAccessoryViewID }} 与它的 nativeID 相同的输入框获得焦点时显示。Rust:input_accessory_view(native_id),在 .input_accessory(native_id) 与之相同的输入框获得焦点时显示。它与窗口等宽、高度由内容决定,并在原生侧随键盘一起移动。

属性类型Rust说明
nativeIDstringinput_accessory_view(native_id)inputAccessoryViewID 为该值的输入框会显示这个工具栏。
styleStyleProp<ViewStyle>Styled 方法(.bg(…))工具栏的样式(例如背景色)。
classstringTailwind class。
childrenJSX.Element.child(…)工具栏的内容。

它在树中所处的位置不渲染任何内容,在 Android 上则完全不渲染,因此可以把它放在所属输入框的旁边。

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())),
            ),
        )
}

另见表单与键盘。

#BackHandler

在 Android 上可用;在 iOS 上,它的监听器会被保留但不会被调用,exitApp() 立即返回。

const BackHandler: {
  on(event: "press", listener: () => boolean | void): () => void;
  exitApp(): void;
};

系统返回键和返回手势。对话框先处理返回操作(关闭对话框);然后运行 BackHandler 的监听器,最后添加的最先运行;都不处理时,最上层的导航栈出栈,没有可返回的页面时应用退出。

成员Rust说明
on("press", listener)on_back_press(listener)listener 处理了这次返回时返回 true。返回一个停止监听的函数;在组件渲染(或 effect 运行)期间添加的监听器随之停止,Rust 与 TSX 都是如此。
exitApp()监听器返回 false退出应用。Rust 监听器返回 false 时,返回操作继续传递,由导航栈返回或应用退出。
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; // 交给应用返回或退出
        }
        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); // 下一次返回即离开
            }
        });
        true
    });
    // ...
}

状态栏在 iOS 和 Android 上都由 nsh/base 中页面和对话框的 statusBar 设置。