导航 API
NavigationStack、NavigationLink、TabView、ToolbarItem。
本页覆盖 NavigationStack、NavigationLink、NavigationSplitView、TabView、Tab、NavigationPath、ToolbarItem 和 ToolbarSpacer。页面级结构优先用这些原生导航容器,不要用手写按钮去模拟系统导航。
#什么时候用
| 目标 | 首选 API | 说明 |
|---|---|---|
| 普通推入详情页 | NavigationStack + NavigationLink | 列表到详情、设置项到二级页。 |
| 代码控制 push/pop | NavigationStack(path=...) + NavigationPath | 登录流程、分步表单、点击完成后跳转。 |
| 多个主栏目 | TabView + Tab | 首页、搜索、我的等顶层导航。 |
| iPad 多列 | NavigationSplitView | 侧边栏 + 详情页,可加 supplementary 第三列。 |
| 顶栏/底栏命令 | .toolbar([...]) + ToolbarItem | 保存、关闭、编辑、分享等页面命令。 |
#最小正确示例
已复制
import appui
items = [
{"id": "a", "title": "Alpha", "status": "Ready"},
{"id": "b", "title": "Beta", "status": "Draft"},
]
def item_key(item):
return item["id"]
def detail_view(item):
return (
appui.Form([
appui.Section("详情", [
appui.LabeledContent("Title", value=item["title"]),
appui.LabeledContent("Status", value=item["status"]),
])
])
.navigation_title(item["title"])
.toolbar([
appui.ToolbarItem(
placement="navigation_bar_trailing",
content=appui.CloseButton(),
role="close",
)
])
)
def row_view(item):
return appui.NavigationLink(
destination=detail_view(item),
label=appui.Label(item["title"], system_image="doc.text"),
)
def body():
root = appui.List([
appui.Section("Items", [
appui.ForEach(items, row_builder=row_view, key=item_key)
])
]).navigation_title("Navigation")
return appui.NavigationStack(root)
appui.run(body)
#导航容器签名
| API | 签名 | 分类 |
|---|---|---|
NavigationStack | NavigationStack(content: Optional[View] = None, path: Optional[NavigationPath] = None, destinations: Optional[Dict[str, Callable]] = None) | navigation |
NavigationView | NavigationView = NavigationStack | 兼容别名 |
NavigationLink | NavigationLink(title: Optional[str] = None, destination: Optional[View] = None, label: Optional[View] = None) | navigation |
NavigationSplitView | NavigationSplitView(sidebar: Optional[View] = None, detail: Optional[View] = None, supplementary: Optional[View] = None, column_visibility: str = 'all') | navigation |
TabView | TabView(tabs: Optional[Sequence["Tab"]] = None, selection: Optional[int] = None, on_change: Optional[Callable] = None, onChange: Optional[Callable] = None) | navigation |
Tab | Tab(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, content: Optional[View] = None, badge: Optional[int] = None, tag: Optional[int] = None, systemImage: Optional[str] = None, role: Optional[str] = None, key: Optional[str] = None) | navigation |
NavigationPath | NavigationPath() | 公开类型 |
ToolbarItem | ToolbarItem(placement: str = 'automatic', content: Optional[View] = None, role: Optional[str] = None) | presentation |
ToolbarSpacer | ToolbarSpacer(sizing: str = 'fixed', placement: str = 'automatic') | presentation |
#NavigationPath 方法
| API | 签名 | 所属类型 |
|---|---|---|
append | append(view_or_value: Union["View", str, int, Dict[str, Any]]) -> None | NavigationPath |
pop | pop(count: int = 1) -> None | NavigationPath |
pop_to_root | pop_to_root() -> None | NavigationPath |
replace | replace(items: Sequence[Any]) -> None | NavigationPath |
已复制
import appui
path = appui.NavigationPath()
def open_profile():
path.append("profile")
def go_root():
path.pop_to_root()
def make_destination(route):
if route == "profile":
return appui.Form([
appui.Section("Profile", [
appui.Text("Programmatic destination"),
appui.Button("Back to root", action=go_root),
])
]).navigation_title("Profile")
return appui.Text("Unknown").navigation_title("Unknown")
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Actions", [
appui.Button("Open profile", action=open_profile),
])
]).navigation_title("Root"),
path=path,
destinations={"profile": make_destination},
)
appui.run(body)
#TabView 示例
已复制
import appui
state = appui.State(tab=0, enabled=True)
def set_tab(value):
state.tab = value
def set_enabled(value):
state.enabled = value
def home_view():
return appui.NavigationStack(
appui.Text("Home").padding().navigation_title("Home")
)
def settings_view():
return appui.NavigationStack(
appui.Form([
appui.Section("Settings", [
appui.Toggle(
"Enabled",
is_on=state.enabled,
on_change=set_enabled,
),
])
]).navigation_title("Settings")
)
def body():
return appui.TabView(
tabs=[
appui.Tab("Home", system_image="house", content=home_view(), tag=0),
appui.Tab("Settings", system_image="gear", content=settings_view(), tag=1),
],
selection=state.tab,
on_change=set_tab,
)
appui.run(body, state=state)
#底部附件与原生 Sheet
持续任务可以用 .tab_view_bottom_accessory(...) 显示底部常驻状态条,再用 .sheet(...) 打开完整面板。这样底部条仍由 iOS 26 TabView 原生区域承载,展开页则交给系统 sheet 处理圆角、拖拽条、下拉关闭和 detents。
已复制
import appui
state = appui.State(show_panel=False)
def open_panel():
state.show_panel = True
def close_panel():
state.show_panel = False
def compact_bar():
return appui.HStack([
appui.Image(system_name="arrow.down.circle.fill").foreground_color("systemBlue"),
appui.VStack([
appui.Text("下载中").font("subheadline").bold(),
appui.Text("42% · 3 个任务").font("caption").foreground_style("secondary"),
], alignment="leading", spacing=2)
.frame(max_width=appui.infinity, alignment="leading"),
appui.Image(system_name="chevron.up").foreground_color("secondaryLabel"),
], spacing=10).padding(horizontal=14, vertical=10)
def expanded_panel():
return appui.NavigationStack(
appui.Form([
appui.Section("任务", [
appui.LabeledContent("当前进度", value="42%"),
appui.LabeledContent("剩余任务", value="3"),
appui.Button("完成", action=close_panel),
])
]).navigation_title("下载")
)
def body():
tabs = appui.TabView(tabs=[
appui.Tab("首页", system_image="house", content=appui.Text("Home"), tag=0),
appui.Tab("设置", system_image="gear", content=appui.Text("Settings"), tag=1),
])
return tabs.tab_view_bottom_accessory(
compact_bar().content_shape("rect").on_tap(open_panel)
).sheet(
is_presented=state.show_panel,
on_dismiss=close_panel,
content=expanded_panel,
detents="medium_large",
drag_indicator="visible",
)
appui.run(body, state=state)
紧凑条会放在底部系统区域;点击后打开原生 sheet。不要再额外给同一个 TabView 挂底部 safe_area_bar,否则会和底部附件争同一块区域。需要更像系统媒体 App 的体验时,优先把完整播放器设计成适合 sheet 的内容,而不是自定义全屏 overlay。
#常用修饰符
| API | 签名 | 所属类型 |
|---|---|---|
.navigation_title | .navigation_title(title: Union[str, "View"]) -> Self | View |
.navigation_bar_title_display_mode | .navigation_bar_title_display_mode(mode: str) -> Self | View |
.navigation_bar_back_button_hidden | .navigation_bar_back_button_hidden(value: bool = True) -> Self | View |
.toolbar | .toolbar(items: Any) -> Self | View |
.toolbar_background | .toolbar_background(visibility: str = 'visible', bars: str = 'navigation_bar') -> Self | View |
.toolbar_color_scheme | .toolbar_color_scheme(scheme: str = 'dark', bars: str = 'navigation_bar') -> Self | View |
.navigation_destination | .navigation_destination(is_presented: bool = False, content: Optional["View"] = None, on_dismiss: Optional[Callable] = None, isPresented: Optional[bool] = None, onDismiss: Optional[Callable] = None) -> Self | View |
.safe_area_bar | .safe_area_bar(edge: str = 'bottom', content: Optional["View"] = None, alignment: str = 'center', spacing: Optional[float] = None, safeAreaEdge: Optional[str] = None) -> Self | View |
.tab_view_bottom_accessory | .tab_view_bottom_accessory(content: Optional["View"] = None, enabled: bool = True, is_enabled: Optional[bool] = None, isEnabled: Optional[bool] = None) -> Self | View |
.tab_bar_minimize_behavior | .tab_bar_minimize_behavior(behavior: str = 'automatic') -> Self | View |
.tab_view_search_activation | .tab_view_search_activation(activation: str = 'search_tab_selection') -> Self | View |
#与相邻 API 的区别
| API | 不同点 |
|---|---|
NavigationLink vs NavigationPath | NavigationLink 适合用户点击某一行进入详情;NavigationPath 适合业务逻辑主动跳转。 |
TabView vs NavigationStack | TabView 是顶层栏目切换;每个 Tab 里面通常再放自己的 NavigationStack。 |
NavigationSplitView vs TabView | NavigationSplitView 是同一任务的多列信息架构;TabView 是多个任务域的顶层切换。 |
.toolbar vs 页面正文按钮 | 页面级命令放工具栏;内容相关动作放在表单、列表或卡片中。 |
.tab_view_bottom_accessory vs .sheet | 前者只显示底部常驻紧凑条;后者打开完整面板、表单或播放器页。 |
.tab_view_bottom_accessory vs .safe_area_bar | 前者是 TabView 的系统底部附件;后者是普通安全区插入内容,不要同时挂在同一个底部区域。 |
CloseButton vs 普通 Button | 关闭 MiniApp 时使用 CloseButton 或 ToolbarItem(role="close"),系统能识别关闭语义。 |
ToolbarSpacer vs 空白 Spacer | ToolbarSpacer 是工具栏项;正文布局留白继续用 Spacer。 |
#常见错误
| 错误 | 正确做法 |
|---|---|
只用 VStack 加按钮模拟页面切换。 | 使用 NavigationStack、NavigationLink 或 NavigationPath。 |
在 TabView 外面只包一个全局 NavigationStack。 | 每个 Tab 里面放自己的 NavigationStack,避免标题和返回栈互相污染。 |
| 用“播放”或“下载”单独占一个 Tab,但又需要全局状态条。 | 用 .tab_view_bottom_accessory(...) 放紧凑状态条,点击后用 .sheet(...) 打开完整面板。 |
| 隐藏返回按钮但没有替代路径。 | 用 toolbar 放明确的返回、取消或关闭按钮。 |
ForEach 行没有稳定 key。 | 给 ForEach 传稳定键函数,避免列表刷新后导航状态错位。 |
ToolbarItem 使用旧 placement 名称。 | 使用 AppUI 文档中的 navigation_bar_trailing。 |