媒体
图片、相册、相机、地图、视频和 WebView 的页面模式。
appui 中与图片、相册、相机、文件导入、视频、网页与地图相关的视图。下列示例均可在应用内 AppUI 预览环境运行。
#预期效果
示例会展示图片、网络图、相册、文件、相机、视频、网页和地图控件如何嵌入 AppUI 页面。
#设计原则
- 资源图用
Image(name=...);SF Symbols 用Image(system_name=...)。 - 网络图用
AsyncImage,占位与失败视图通过子视图传入。 - 相册 / 相机返回的是设备上的文件路径字符串(由系统写入临时目录后回调)。
- Files App / 文档提供方用
FileImporter,默认复制到 App 可访问位置后返回路径列表。 - WebView 只能二选一:
url或html。 - MapView 的
markers为字典列表,键包括latitude、longitude、title。
#Image:本地资源与 SF Symbol
Image 支持链式修饰:resizable()、aspect_ratio(ratio, content_mode)、symbol_rendering_mode(mode)、image_scale(scale)。
import appui
state = appui.State(kind="SF Symbol")
def body():
return appui.NavigationStack(
appui.VStack([
appui.Text(state.kind).font("headline"),
appui.Image(system_name="star.fill")
.resizable()
.aspect_ratio(content_mode="fit")
.symbol_rendering_mode("palette")
.image_scale("large")
.frame(width=56, height=56)
.foreground_color("systemYellow"),
appui.Label("设置", system_image="gear"),
], spacing=16).padding()
.navigation_title("Image")
)
appui.run(body, state=state, presentation="sheet")
content_mode 为 'fit' 或 'fill'。symbol_rendering_mode 常用:'hierarchical'、'palette'、'multicolor'(未识别时回退为单色)。
image_scale 为 'small'、'medium'(默认)、'large'。
#AsyncImage:网络图片
参数:url、placeholder、error_view、content_mode、on_success、on_failure。占位与错误视图为子节点(前两个子视图依次为占位、错误)。
import appui
state = appui.State(status="等待加载")
def image_loaded():
state.status = "图片已加载"
def image_failed():
state.status = "图片加载失败"
def body():
return appui.NavigationStack(
appui.VStack([
appui.AsyncImage(
url="https://www.apple.com/ac/structured-data/images/knowledge_graph_logo.png",
placeholder=appui.ProgressView(label="Loading"),
error_view=appui.Image(system_name="exclamationmark.triangle"),
content_mode="fit",
on_success=image_loaded,
on_failure=image_failed,
).frame(height=120),
appui.LabeledContent("状态", value=state.status),
], spacing=16).padding()
.navigation_title("AsyncImage")
)
appui.run(body, state=state, presentation="sheet")
#PhotoPicker:相册
selection_limit:1 表示单选;0 表示不限制(以系统行为为准)。filter:'images'、'videos'、'all'。on_picked 收到 路径列表。
import appui
state = appui.State(paths=[])
def on_picked(paths):
state.paths = paths or []
def body():
return appui.VStack([
appui.Text("已选: " + (" | ".join(state.paths) if state.paths else "无")).font("caption"),
appui.PhotoPicker(
selection_limit=1,
filter="images",
on_picked=on_picked,
label=appui.Label("选择照片", system_image="photo.on.rectangle"),
),
], spacing=12).padding()
appui.run(body, state=state, presentation="sheet")
#FileImporter:文件导入
allowed_types 可传类型名、扩展名或 MIME 类型,例如 'text'、'pdf'、'csv'、'image/png'。allows_multiple=True 允许多选。copy=True 是默认值,表示先复制进 App 可访问位置,再把路径列表传给 on_picked。
import appui
state = appui.State(files=[])
def on_files(paths):
state.files = paths or []
def body():
rows = [
appui.Text(path).font("caption").line_limit(1)
for path in state.files
]
return appui.NavigationStack(
appui.Form([
appui.Section("导入", [
appui.FileImporter(
allowed_types=["text", "pdf", "csv"],
allows_multiple=True,
on_picked=on_files,
label=appui.Label("选择文件", system_image="doc.badge.plus"),
),
]),
appui.Section("文件", rows or [
appui.ContentUnavailableView("暂无文件", system_image="doc", description="从系统文件选择器导入")
]),
]).navigation_title("文件导入")
)
appui.run(body, state=state, presentation="sheet")
文件选择由系统界面完成;用户取消时通常不会触发有效路径。需要读取文件内容时,在回调里保存路径,再在按钮动作、刷新函数或后台流程中读取,避免在 body() 里同步读大文件。
#CameraPicker:相机
source:'camera' 或 'front'。media_type:'photo' 或 'video'。on_captured 收到单个路径字符串。
import appui
state = appui.State(last="")
def on_captured(path):
state.last = path or ""
def body():
return appui.VStack([
appui.Text(state.last or "尚未拍摄").font("caption"),
appui.CameraPicker(
source="camera",
media_type="photo",
on_captured=on_captured,
label=appui.Label("拍照", system_image="camera.fill"),
),
], spacing=12).padding()
appui.run(body, state=state, presentation="sheet")
#VideoPlayer:AVKit
url 可为远程 HTTPS 或应用内可访问的本地文件名。autoplay、loop、show_controls 控制播放行为。
只展示视频时直接用 VideoPlayer(url=...)。如果页面需要播放/暂停/seek、保存进度、倍速、PiP 状态或切集,使用 PlayerController 并传给 VideoPlayer(player=player);AppUI 新页面不要再 import avplayer 控制同一块内嵌视频。
import appui
state = appui.State(status="可播放")
def body():
return appui.NavigationStack(
appui.VStack([
appui.VideoPlayer(
url="https://media.w3.org/2010/05/sintel/trailer.mp4",
autoplay=False,
loop=False,
show_controls=True,
).frame(height=220),
appui.LabeledContent("状态", value=state.status),
], spacing=16).padding()
.navigation_title("VideoPlayer")
)
appui.run(body, state=state, presentation="sheet")
#WebView:URL 或 HTML
import appui
state = appui.State()
def body():
return appui.WebView(
html="<html><body style='font-family:system-ui'><h1>appui</h1><p>内嵌 HTML</p></body></html>"
).frame(height=360).padding()
appui.run(body, state=state, presentation="sheet")
远程页面示例:appui.WebView(url="https://www.apple.com").frame(height=360)。
#MapView:地图与标注
默认中心为旧金山坐标。span 为经纬跨度。map_style 可为 'automatic'(默认,不传样式)、'standard'、'satellite'、'hybrid'。
import appui
state = appui.State()
def body():
return appui.MapView(
latitude=37.7749,
longitude=-122.4194,
span=0.08,
markers=[
{"latitude": 37.78, "longitude": -122.40, "title": "标记 A"},
],
map_style="standard",
).frame(height=280).padding()
appui.run(body, state=state, presentation="sheet")
#与旧版 ui 模块对照(迁移)
ui 为 Pythonista 风格命令式 API;同一应用内可对照理解差异:
import ui
v = ui.View()
b = ui.Button(title="Go")
def on_button(sender):
pass
b.action = on_button
v.add_subview(b)
# v.present('sheet') # 需在 AppUI 预览环境中调用
appui 用声明式 body() 返回 View,由 appui.run(body, state=..., presentation="sheet") 呈现。
#小结
| 视图 | 典型用途 |
|---|---|
Image | 资源图、SF Symbol、缩放与宽高比 |
AsyncImage | 网络图片与阶段 UI |
PhotoPicker / CameraPicker | 系统相册与相机,路径回调 |
FileImporter | 系统文件选择器,路径列表回调 |
VideoPlayer | 流媒体或本地视频 |
WebView | url 或 html 嵌入网页 |
MapView | 坐标、跨度、标注与样式 |
更完整的参数说明见同目录下的 appui-ref-media.md。