导航与页面结构
TabView、NavigationStack、NavigationLink 和 toolbar 的组合方式。
导航的目标是让页面关系清楚:栈式跳转用 NavigationStack,多 tab 用 TabView,iPad 双栏用 NavigationSplitView,临时任务用 sheet / alert / popover。
#预期效果
示例会展示固定跳转、数据驱动导航、Tab、sheet 和 alert 的页面层级。
#选择表
| 需求 | API |
|---|---|
| 页面逐层进入、支持返回 | NavigationStack |
| 固定列表进入详情 | NavigationLink |
| 通过数据驱动跳转 | NavigationPath + destinations |
| 底部标签页 | TabView / Tab |
| iPad / Mac 侧栏 + 详情 | NavigationSplitView |
| 临时编辑页 | .sheet(...) |
| 强制流程 | .full_screen_cover(...) |
| 确认、错误、危险操作 | .alert(...) / .confirmation_dialog(...) |
| 小浮层 | .popover(...) |
#NavigationStack
最小写法是给根内容包一层 NavigationStack,再在子页面里设置标题。
已复制
import appui
state = appui.State(last_opened="None")
def mark_opened(title):
state.last_opened = title
def settings_page():
def open_settings():
mark_opened("设置")
return appui.Form([
appui.Section("设置", [
appui.Text("这里是详情内容"),
appui.Button("记录打开", action=open_settings),
]),
appui.Section("状态", [
appui.LabeledContent("Last opened", value=state.last_opened),
]),
]).navigation_title("设置")
def about_page():
return appui.Text("实时快路径 appui").padding().navigation_title("关于")
def body():
return appui.NavigationStack(
appui.List([
appui.NavigationLink(title="设置", destination=settings_page()),
appui.NavigationLink(title="关于", destination=about_page()),
]).navigation_title("首页")
)
appui.run(body, state=state)
固定页面跳转用 NavigationLink;动态路由用 NavigationPath。
#NavigationPath
NavigationPath 适合“从按钮打开指定页面”“根据 tag 和 data 打开详情”等数据驱动场景。
已复制
import appui
path = appui.NavigationPath()
state = appui.State(opened="None")
def open_settings():
path.append({"tag": "settings", "data": None})
def open_user():
path.append({"tag": "user", "data": 42})
def go_back():
path.pop()
def go_home():
path.pop_to_root()
def settings_destination(data):
return appui.VStack([
appui.Text("设置").font("title2"),
appui.Button("返回", action=go_back),
], spacing=12).padding().navigation_title("设置")
def user_destination(user_id):
return appui.VStack([
appui.Text(f"用户 {user_id}").font("title2"),
appui.Button("返回首页", action=go_home),
], spacing=12).padding().navigation_title("用户")
def body():
return appui.NavigationStack(
content=appui.VStack([
appui.Text("首页").font("title2"),
appui.Button("打开设置", action=open_settings).button_style("bordered"),
appui.Button("打开用户 42", action=open_user).button_style("bordered_prominent"),
], spacing=16).padding().navigation_title("示例"),
path=path,
destinations={
"settings": settings_destination,
"user": user_destination,
},
)
appui.run(body, state=state)
path.append({"tag": "...", "data": ...}) 的 tag 用来找目标构建函数,data 会传给目标函数。
#TabView
Tab 适合并列的顶层区域,不要用来承载一次性流程。每个 tab 中如果还需要进入详情,再单独放自己的 NavigationStack。
已复制
import appui
state = appui.State(selection=0)
def set_selection(value):
state.selection = value
def home_view():
return appui.NavigationStack(
appui.List([
appui.Section("Home", [
appui.Text("Home content")
])
]).navigation_title("首页")
)
def settings_view():
return appui.NavigationStack(
appui.Form([
appui.Section("Settings", [
appui.LabeledContent("Selected tab", value=str(state.selection))
])
]).navigation_title("设置")
)
def body():
return appui.TabView(
selection=state.selection,
on_change=set_selection,
tabs=[
appui.Tab("首页", system_image="house", tag=0, content=home_view()),
appui.Tab("设置", system_image="gearshape", tag=1, content=settings_view()),
],
)
appui.run(body, state=state)
#Sheet、Alert、ConfirmationDialog
弹层用状态控制显示和关闭。复杂 sheet 内容抽成命名函数。
已复制
import appui
state = appui.State(show_sheet=False, show_alert=False, name="")
def set_name(value):
state.name = value
def open_sheet():
state.show_sheet = True
def close_sheet():
state.show_sheet = False
def open_alert():
state.show_alert = True
def close_alert():
state.show_alert = False
def editor_sheet():
return appui.NavigationStack(
appui.Form([
appui.Section("编辑", [
appui.TextField("名称", text=state.name, on_change=set_name),
appui.Button("完成", action=close_sheet).button_style("bordered_prominent"),
])
]).navigation_title("编辑")
)
def body():
root = (
appui.VStack([
appui.Button("编辑", action=open_sheet).button_style("bordered"),
appui.Button("删除", action=open_alert)
.foreground_color("systemRed"),
], spacing=16)
.padding()
.sheet(is_presented=state.show_sheet, on_dismiss=close_sheet, content=editor_sheet)
.alert(
"确认删除?",
is_presented=state.show_alert,
on_dismiss=close_alert,
actions=[
appui.Button("取消", role="cancel", action=close_alert),
appui.Button("删除", role="destructive", action=close_alert),
],
message="删除后无法恢复。",
)
)
return appui.NavigationStack(root.navigation_title("呈现"))
appui.run(body, state=state)
普通编辑用 sheet,破坏性操作先 alert 或 confirmation dialog。
#NavigationSplitView
NavigationSplitView 适合大屏。窄屏上系统会自动退化为栈式体验。
已复制
appui.NavigationSplitView(
sidebar=appui.List([...]).navigation_title("项目"),
detail=appui.Text("选择一个项目").navigation_title("详情"),
)
如果目标设备主要是 iPhone,不要为了“看起来高级”强行使用 SplitView。
#常见问题
| 问题 | 处理 |
|---|---|
| 页面没有标题 | 在内容 View 上加 .navigation_title(...)。 |
| 返回后状态丢失 | 把状态放在外层 State,不要在目标页面函数里重新创建。 |
destinations 回调报参数错 | 回调写成接收 data 的命名函数。 |
| sheet 点了没反应 | 确认 is_presented 读取的是状态字段,关闭时会把字段改回 False。 |
| Tab 和导航栈混乱 | Tab 做顶层分区,Stack 做分区内跳转。 |
| 工具栏图标按钮报错 | Button 的图标放在 content=Image(...) 或 content=Label(...)。 |