本頁由機器翻譯,可能有錯誤。 閱讀英文原文

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 在手機、桌面與網頁上共用相同的行為與 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() 會建立分頁列,每個屬性有對應的設定方法(.items(..)、.selected_index(..)),每個事件有對應的監聽器(.on_change(..)),並提供具型別的樣式方法;TabBarItem::new(title).icon(name) 則用於建立分頁。

#屬性

屬性Rust型別預設值說明
items.items(..)TabBarItem[][]依序排列的分頁。
selectedIndex.selected_index(..)number0以索引指定選取的分頁;超出分頁數量的索引表示不選取任何分頁。
hidden.hidden(..)booleanfalseiOS:不使用轉場動畫直接隱藏分頁列。此值在 push 與 pop 之間由應用程式負責維護。當此屬性與可見堆疊的 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)與網頁:各區段以 Tabs 顯示,採用應用程式自己的外觀。TabBar 在這些平台上不佔用空間,因此同一份版面配置即可涵蓋兩種情況,並由 platform.os 選擇(nsh 的裝置 API)。

#InputAccessoryView

適用於 iOS 與 Mac Catalyst;在 Android 上是一個空的隱藏 view。

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 類別。
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 的監聽器,最近新增的優先;若沒有任何監聽器處理,最上層的導覽堆疊會 pop,若已無頁面可返回,應用程式便會結束。

成員Rust說明
on("press", listener)on_back_press(listener)listener 在已處理該操作時返回 true。會返回一個停止監聽的函式;在元件渲染(或 effect 執行)期間新增的監聽器,會隨之停止,Rust 與 TSX 皆然。
exitApp()監聽器返回 false離開應用程式。返回 false 的 Rust 監聽器會讓該操作繼續傳遞,因此堆疊會返回或應用程式會結束。
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
    });
    // ...
}

狀態列由 nsh/base 中頁面與對話框的 statusBar 設定,iOS 與 Android 皆然。