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

文件

平台專用程式碼

一個 NSH bundle 可在 iOS、Android、macOS、Windows 和 Linux(以及 Mac Catalyst)上運行,並可透過網頁運行環境在瀏覽器中運行。大部分 app 完全不需要平台檢查:元素、樣式、佈局和模組在所有平台上行為一致,由它們組成的元件在各處外觀也相同。當某些事物確實只屬於某個平台時,NSH 為它提供明確的位置,令平台程式碼保持可見而且自成一體:

Rust TSX 用途
建構器方法(.ripple(..)、.input_accessory(..)) ios={{…}}、android={{…}}、desktop={{…}}、web={{…}} 只有單一平台才有的 props。
on_back_press、nsh_core::element::desktop nsh/mobile、nsh/desktop 只有流動平台或只有桌面平台才有的 API。
os()、idiom()、is_desktop() platform.os、platform.desktop、platform.select 按平台選擇值或程式碼。
.when(os() == Os::Ios, ..) ios: / android: / web: class 按平台設定樣式。

#桌面與流動裝置

每個平台都屬於桌面或流動,規則只有一條。桌面包括 macOS、Windows、Linux,以及以桌面形式(滑鼠與寬視窗,網頁)運行的瀏覽器。流動包括 iOS、Android,以及以手機或平板形式運行的瀏覽器;Mac Catalyst 運行 iOS 後端,所以也屬於流動,並使用 ios={{…}} props。desktop={{…}} props、platform.select({ desktop, mobile })、platform.desktop 和 platform.idiom === "desktop" 全都依循這條規則,nsh/base 的元件亦然(選單在桌面上是彈出式選單,在流動裝置上是 sheet),在 Rust 中還有 is_desktop()。

nsh/desktop 的 isAvailable 回答的是一個較窄的問題:該模組的視窗與選單 API 是否可用,在 macOS、Windows 和 Linux 上可用。佈局、密度和輸入請以 platform.desktop 作判斷,Window 和 Menu 的呼叫則以 isAvailable 作判斷。

#平台專屬 props

在 Rust 中,只有某個平台才有的 prop 就是一個普通的 builder 方法(.ripple(..);Android 的 .elevation(..);iOS 的 .input_accessory(..);桌面平台的 .window_drag(..));其他平台會略過它。在 TSX 中,只存在於單一平台的 props 會歸入以該平台命名的物件。每個元素都接受 ios、android、desktop(macOS、Windows、Linux,以及桌面形式的瀏覽器)和 web(瀏覽器);在其他平台上該物件會被忽略,在所屬平台上只會套用其中的鍵。桌面瀏覽器會同時套用 desktop 和 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(…)
})

TSX 物件如同其他 prop 一樣具反應性;從物件中移除的鍵會被清除。

目前已有的項目:

Rust TSX 於 說明
.ripple(Ripple { .. }) android={{ ripple }} 每個元素 按下時的 Material 水波紋:顏色、無邊界、半徑、前景。
.elevation(..) android={{ elevation }} 每個元素 Material 陰影,單位為 point,依據 view 的形狀。
android={{ accessibilityLiveRegion }} 每個元素 "none"、"polite" 或 "assertive":TalkBack 會播報該 view 的變化。
.status_bar_translucent(..) android={{ statusBarTranslucent }} Dialog 對話框的內容延伸至狀態列下方。
.input_accessory(..) ios={{ inputAccessoryViewID }} Input 顯示於鍵盤上方的 InputAccessoryView。
.auto_fill(..) ios={{ textContentType }} Input iOS 對自動填寫類型的命名(Rust 的 AutoFill 同時涵蓋兩個平台)。
.supported_orientations(..) ios={{ supportedOrientations }} Dialog 所允許的方向。
.back_forward_gestures(..) ios={{ allowsBackForwardNavigationGestures }} WebView 在頁面記錄中滑動返回及前進。
.header_right_system_item(..) ios={{ headerRightSystemItem }} NavPage UIKit 自家的列按鈕,位於標題列末端,按下時觸發 headerRight。
.header_left_system_item(..) ios={{ headerLeftSystemItem }} NavPage UIKit 自家的列按鈕,位於標題列起端,按下時觸發 headerLeft。
.header_right_symbol(..) ios={{ headerRightSymbol }} NavPage UIKit 自家的列按鈕,位於標題列末端並顯示 SF Symbol,按下時觸發 headerRight。
.header_left_symbol(..) ios={{ headerLeftSymbol }} NavPage UIKit 自家的列按鈕,位於標題列起端並顯示 SF Symbol,按下時觸發 headerLeft。
.back_forward_gestures(..) desktop={{ allowsBackForwardNavigationGestures }} WebView macOS:雙指觸控板滑動可在頁面記錄中返回及前進;Windows:觸控螢幕滑動;Linux:觸控板滑動。
.window_drag(..) desktop={{ windowDrag }} 每個元素 按下時會拖動視窗,如同系統標題列一般(見 nsh/desktop)。

在 TSX 中,desktop={{…}} 和 web={{…}} 在所有平台上都可使用。型別(IOSProps、AndroidProps、DesktopProps 及其各元素形式)會讓編輯器知道每個元素接受甚麼。

在所有平台上都有意義的元素行為屬於頂層 prop(在 Rust 中是同名方法),並在有意義之處套用:NavPage 的 title、headerStyle、headerLargeTitle 和 headerBackTitle 負責設定 iOS 和 Android 的系統標題列,它的 header slot 則是桌面平台和網頁的標題列,而 Dialog 的 presentation 用來選擇 iOS 的 sheet。AlertDialog.show 的 closeOnBackdropPress、pagingEnabled、refreshing、hitSlop 等在所有平台上都適用。某平台自有的系統功能放在它的物件內;若多個平台都有,則在各自的物件中以相同名稱出現:WebView 的 allowsBackForwardNavigationGestures(系統在頁面記錄中的滑動)見於 ios={{…}} 和 desktop={{…}}。

#平台模組

在 Rust 中,只有單一平台才有的 API 是 nsh_core::prelude(on_back_press、input_accessory_view)和 nsh_core::element::desktop 的函式(視窗:set_title、set_title_bar、title_bar、minimize、toggle_maximize、toggle_full_screen、set_frame、set_level、order_front、close_window;選單:set_application_menu、popup_menu);在其他平台上它們不會執行任何操作。在 TSX 中,nsh/mobile 包含 iOS 和 Android 的 API(部分只屬其中一個平台),nsh/desktop 則包含桌面平台的 API。兩者在所有平台上都可匯入,因此同一個 bundle 可到處運行:在其他平台上,它們的函式會立即返回,元件則不佔空間。各自匯出 isAvailable,在其所屬平台上為 true;platform.os 可區分兩者。

#nsh/mobile

isAvailable 在 iOS(包括 Mac Catalyst)和 Android 上為 true。兩個平台都有 TabBar,即系統的分頁列。

#BackHandler (Android)

Rust TSX 說明
on_back_press(listener) BackHandler.on("press", listener) 在對話框之後、堆疊之前監聽返回按鈕;處理完畢時返回 true。監聽器會隨加入它的元件(或 effect)一同移除;在 TSX 中,該呼叫亦會返回一個函式,可提早停止監聽。
BackHandler.exitApp() 離開 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
    });
    // …
}

返回按鈕會先由最上層的對話框處理,然後是返回監聽器(最近加入的優先),再到最上層的導覽堆疊,若無人處理,app 便會退出。參閱導覽。

#InputAccessoryView (iOS)

Rust TSX 說明
input_accessory_view(native_id) InputAccessoryView 在鍵盤上方顯示的列,供指定其原生 id 的輸入框使用(.input_accessory(..)、ios={{ inputAccessoryViewID }})。

參閱表單與鍵盤。

#nsh/desktop

Rust app 以 nsh_core::element::desktop 繪製自己的標題列(以 Rust 建構)。TSX 的模組:

匯出 說明
Window 視窗:setTitle、用於 app 自訂標題列的 setTitleBar({ style: "hidden" })、minimize、toggleMaximize、toggleFullScreen、close。
titleBar 隱藏標題列的高度,以及系統視窗控制項所佔的空間(具反應性)。
Menu setApplicationMenu(items)(附快速鍵的選單列)和 popup(items, at?)(右鍵選單)。
isAvailable true 於視窗和選單 API 可用的平台:macOS、Windows 和 Linux。桌面瀏覽器為 platform.desktop,並保留瀏覽器自身的視窗和選單。

參閱 nsh/desktop。

#狀態列

在兩個平台上,狀態列都屬於最上層的頁面或對話框,與 UIKit 一樣,由最上層的 view controller 決定:NavPage 和 Dialog 接受 statusBar(在 Rust 中是 .status_bar(StatusBarStyle::Light),其他欄位則用 StatusBarSetting),包含 foreground("auto",深色背景用 "light",淺色背景用 "dark")、hidden 和 animated。最近顯示的頁面或對話框逐個欄位優先,當它消失後,其下方的一個會重新生效。nsh.json 的 "statusBar" 是它們之下 app 的預設值。

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

在嵌入原生 app 的 app 中,iOS 上由宿主 app 的 view controller 決定狀態列。在 nsh.json 中宣告 "capabilities" 的 app,需要 "statusBar": true 才能更改。

#platform.os and platform.select

Rust 中的 os() 和 TSX 中來自 nsh 的 platform 常數會告訴你 app 在哪裏運行:Os::Ios、Os::Android、Os::MacOs、Os::Windows、Os::Linux 或 Os::Web,而 platform.os 為 "ios"、"android"、"macos"、"windows"、"linux" 或 "web"(Mac Catalyst 為 "ios",在其任一形式下都帶有 platform.catalyst;macOS 後端是 "macos",Linux 後端是 "linux",Windows 後端是 "windows",它們的元素會套用 desktop props;瀏覽器是 "web",當 platform.desktop 為 true 時亦會套用 desktop props)。版本、形式及其他資訊請參閱裝置與環境。

use nsh_core::{element::Os, prelude::*};

if os() == Os::Android {
    // …
}

#platform.select

在 Rust 中,由 match os() 選擇值,桌面平台用 is_desktop()。在 TSX 中,platform.select(spec) 返回適用於此平台的值。它會先找該 OS 自己的鍵,然後按上述規則找 desktop 或 mobile(platform.desktop),再找 native(除網頁外的所有平台),最後找 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",
};

當沒有任何匹配且沒有 default 時,它返回 undefined。

#渲染不同的元件

以相同方式選擇元件;若選擇永不改變,則在模組層級(TSX)進行:

fn spinner() -> AnyElement {
    if os() == Os::Ios {
        ios_spinner().into_element()
    } else {
        material_spinner().into_element()
    }
}

或以 when / <If> 內聯:

when(os() == Os::Ios, || input_accessory_view("bar").child(…))
    .otherwise(|| text("Android has no input accessories."))

#Tailwind 的平台變體

Rust 以 .when(os() == Os::Ios, |v| ..) 按平台設定樣式;在 TSX 中,ios:、android: 和 web: 變體會在單一平台上套用 class:

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

#優先使用共通行為

在使用平台檢查之前,先考慮這個差異對使用者是否重要。元素和樣式在應有差異之處(捲動物理效果、文字渲染、鍵盤、無障礙功能)已對應各平台的原生行為,而在使用者期望一致之處(你 app 自身的外觀)則保持相同。平台檢查最好只留給使用者所依賴的平台慣例:Android 的返回按鈕和 toast、iOS 的 sheet 和大標題、自動填寫名稱、Android 的水波紋。

#相關內容