数据展示 API
List、ForEach、Form、Section、Table、ProgressView 等数据展示视图。
本页覆盖列表、表单、分组、表格、空状态、进度、链接、角标、时间刷新和富文本。数据展示页面要优先使用系统结构:列表用 List + Section + ForEach,设置页用 Form + Section,不要用普通 VStack 手写整套表单外观。
#什么时候用
| 目标 | 首选 API | 说明 |
|---|---|---|
| 行列表 | List + ForEach | 可滚动、可分组、可加滑动操作。 |
| 设置页 | Form + Section | 原生设置样式,适合输入控件和开关。 |
| 标题分组 | Section | 用 header / footer 表达分组标题和说明。 |
| 折叠内容 | DisclosureGroup | 可展开详情、更多设置。 |
| 标签值行 | LabeledContent | 版本号、状态、统计值。 |
| 多列表格 | Table | iPad 或大屏数据表;iPhone 会按系统能力降级。 |
| 空状态 | ContentUnavailableView | 无数据、无搜索结果、加载失败。 |
| 进度 | ProgressView / Gauge | 加载状态、百分比、容量。 |
| 链接和分享 | Link / ShareLink | 打开系统浏览器或分享面板。 |
| 周期刷新文字 | TimelineView | 时钟、倒计时等低频时间展示。 |
| 富文本 | AttributedText | 同一段文本内混合颜色、字重、斜体和链接。 |
#最小正确示例
已复制
import appui
state = appui.State(
items=[
{"id": "a", "title": "Alpha", "done": False},
{"id": "b", "title": "Beta", "done": True},
],
selected="",
)
def item_key(item):
return item["id"]
def select_item(item):
state.selected = item["title"]
def row_view(item):
def choose():
select_item(item)
icon = "checkmark.circle.fill" if item["done"] else "circle"
return appui.Button(
action=choose,
content=appui.Label(item["title"], system_image=icon),
)
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Items", [
appui.ForEach(state.items, row_builder=row_view, key=item_key)
]),
appui.Section("Selection", [
appui.LabeledContent("Selected", value=state.selected or "None"),
]),
]).navigation_title("Data")
)
appui.run(body, state=state)
#列表和表单
| API | 签名 | 分类 |
|---|---|---|
List | List(content: Optional[Sequence[View]] = None) | collection |
ForEach | ForEach(data: Any, row_builder: Optional[Callable] = None, key: Optional[Callable] = None, rowBuilder: Optional[Callable] = None, content: Optional[Callable] = None) | collection |
Form | Form(content: Optional[Sequence[View]] = None) | collection |
Section | Section(content: Optional[ViewChild] = None, *, header: Optional[Union[str, View]] = None, footer: Optional[Union[str, View]] = None, children: Optional[ViewChild] = None, key: Optional[str] = None) | collection |
已复制
import appui
state = appui.State(notify=True, email="ada@example.com")
def set_notify(value):
state.notify = value
def set_email(value):
state.email = value
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Account", [
appui.TextField(
"Email",
text=state.email,
on_change=set_email,
keyboard_type="emailAddress",
),
appui.Toggle("Notifications", is_on=state.notify, on_change=set_notify),
], footer="Used for account notifications."),
]).navigation_title("Form")
)
appui.run(body, state=state)
#分组和详情
| API | 签名 | 分类 |
|---|---|---|
GroupBox | GroupBox(label: Optional[str] = None, content: Optional[ViewChild] = None, children: Optional[Sequence[View]] = None) | collection |
DisclosureGroup | DisclosureGroup(label: str = '', is_expanded: Optional[bool] = None, content: Optional[ViewChild] = None, isExpanded: Optional[bool] = None, children: Optional[ViewChild] = None) | collection |
LabeledContent | LabeledContent(label: str = '', value: Optional[str] = None, content: Optional[View] = None) | collection |
ControlGroup | ControlGroup(label: str = '', content: Optional[Sequence[View]] = None, children: Optional[Sequence[View]] = None) | control |
已复制
import appui
def body():
return appui.NavigationStack(
appui.Form([
appui.Section("Summary", [
appui.LabeledContent("Version", value="1.0"),
appui.LabeledContent("Status", content=appui.Badge(text="New")),
]),
appui.Section("Details", [
appui.DisclosureGroup(
"Advanced",
is_expanded=False,
content=[
appui.GroupBox(
"Cache",
content=[appui.Text("Local cache is enabled.")],
)
],
)
]),
]).navigation_title("Groups")
)
appui.run(body)
#表格
Table(data=None, columns=None, on_select=None, onSelect=None)
| 参数 | 类型 | 说明 |
|---|---|---|
data | list[dict] | 表格行。 |
columns | list[dict] | 每列字典至少包含 title 和 key。 |
on_select | Callable / None | 选中行回调,接收行字典。 |
已复制
import appui
state = appui.State(selected="")
rows = [
{"id": 1, "name": "Ada", "role": "Admin"},
{"id": 2, "name": "Lin", "role": "Editor"},
]
def select_row(row):
state.selected = row["name"]
def body():
return appui.NavigationStack(
appui.VStack([
appui.Table(
data=rows,
columns=[
{"title": "ID", "key": "id"},
{"title": "Name", "key": "name"},
{"title": "Role", "key": "role"},
],
on_select=select_row,
),
appui.Text(state.selected or "No selection")
.font("footnote")
.foreground_color("secondaryLabel"),
], spacing=12)
.padding()
.navigation_title("Table")
)
appui.run(body, state=state)
#空状态、进度、链接和分享
| API | 签名 | 分类 |
|---|---|---|
ContentUnavailableView | ContentUnavailableView(title: str = '', system_image: Optional[str] = None, description: Optional[str] = None, systemImage: Optional[str] = None) | presentation |
ProgressView | ProgressView(label: Optional[str] = None, value: Optional[float] = None, total: float = 1.0) | feedback |
Link | Link(title: str = '', url: str = '') | control |
Gauge | Gauge(value: float = 0.0, min_value: float = 0.0, max_value: float = 1.0, label: str = '', minValue: Optional[float] = None, maxValue: Optional[float] = None) | feedback |
ShareLink | ShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None) | control |
Badge | Badge(count: Optional[int] = None, text: Optional[str] = None) | presentation |
已复制
import appui
def body():
return appui.NavigationStack(
appui.List([
appui.Section("Status", [
appui.ProgressView(label="Loading"),
appui.ProgressView(label="Progress", value=0.4, total=1.0)
.progress_view_style("linear"),
appui.Gauge(value=0.72, min_value=0.0, max_value=1.0, label="Storage")
.gauge_style("accessory_circular"),
]),
appui.Section("Links", [
appui.Link(title="Apple", url="https://www.apple.com"),
appui.ShareLink(item="Shared from AppUI", subject="AppUI", message="Hello"),
]),
appui.Section("Empty", [
appui.ContentUnavailableView(
title="No Results",
system_image="magnifyingglass",
description="Try another keyword.",
),
]),
]).navigation_title("Status")
)
appui.run(body)
#TimelineView
TimelineView(interval=1.0, content=None) 会按间隔刷新它的子视图,适合时钟、轻量倒计时等低频时间展示。content 是一个 View,不是视图列表。
已复制
import appui
import time
def clock_text():
return appui.Text(time.strftime("%H:%M:%S")).font("title")
def body():
return appui.NavigationStack(
appui.TimelineView(interval=1.0, content=clock_text())
.padding()
.navigation_title("Clock")
)
appui.run(body)
#AttributedText
AttributedText(spans=None) 用 span 字典描述富文本。
| 键 | 类型 | 说明 |
|---|---|---|
text | str | 显示文字。 |
font_size | float | 字号。 |
weight | str | 字重,例如 "bold"、"semibold"、"regular"。 |
bold | bool | 是否加粗。 |
italic | bool | 是否斜体。 |
color | str | 颜色,例如 "systemBlue"、"#FF0000"。 |
link | str | 链接 URL。 |
underline | bool | 下划线。 |
strikethrough | bool | 删除线。 |
已复制
import appui
def body():
return appui.NavigationStack(
appui.AttributedText(spans=[
{"text": "AppUI ", "font_size": 20, "weight": "bold"},
{"text": "rich text", "font_size": 20, "color": "systemBlue"},
{"text": " with link", "link": "https://www.python.org", "underline": True},
])
.padding()
.navigation_title("Rich Text")
)
appui.run(body)
#与相邻 API 的区别
| API | 不同点 |
|---|---|
List vs Form | List 展示数据行;Form 展示设置和输入。 |
Section vs GroupBox | Section 是列表/表单分组;GroupBox 是内容盒。 |
ProgressView vs Gauge | ProgressView 强调加载或进度;Gauge 强调数值状态和容量。 |
Badge vs .badge(...) | Badge 是一个独立视图;.badge(...) 给支持角标的位置添加角标。 |
Link vs WebView | Link 打开外部浏览器;WebView 在 AppUI 内显示网页。 |
TimelineView vs Timer | TimelineView 刷新视图显示;Timer 运行状态更新逻辑。 |
#常见错误
| 错误 | 正确做法 |
|---|---|
ForEach 没有稳定键,列表刷新后行状态错位。 | 传 key 函数,或保证数据里有稳定 id / key。 |
在 Section 里再嵌套完整 List。 | 外层用一个 List,里面放多个 Section。 |
把设置页写成 VStack + Text + Toggle。 | 用 Form([Section(...)])。 |
TimelineView 的 content 传列表。 | 传一个 View,多视图时包进 VStack。 |
ProgressView(label=appui.Text(...))。 | label 传字符串;自定义说明放在相邻 Text。 |