PythonIDE Docs
中文
简体中文

媒体

图片、相册、相机、地图、视频和 WebView 的页面模式。

appui 中与图片、相册、相机、文件导入、视频、网页与地图相关的视图。下列示例均可在应用内 AppUI 预览环境运行。

#预期效果

示例会展示图片、网络图、相册、文件、相机、视频、网页和地图控件如何嵌入 AppUI 页面。

#设计原则

  • 资源图Image(name=...)SF SymbolsImage(system_name=...)
  • 网络图AsyncImage,占位与失败视图通过子视图传入。
  • 相册 / 相机返回的是当前 MiniApp 会话拥有的临时文件路径字符串;普通页面重建不会删除,MiniApp 停止时自动清理。
  • Files App / 文档提供方FileImporter,默认复制到 App 可访问位置后返回路径列表。
  • WebView 只能二选一:urlhtml
  • MapViewmarkers 为字典列表,键包括 latitudelongitudetitle

#Image:本地资源与 SF Symbol

Image 支持链式修饰:resizable()aspect_ratio(ratio, content_mode)symbol_rendering_mode(mode)image_scale(scale)

python
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:网络图片

参数:urlplaceholdererror_viewcontent_modeon_successon_failure。占位与错误视图为子节点(前两个子视图依次为占位、错误)。

python
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_limit1 表示单选;0 表示不限制(以系统行为为准)。filter'images''videos''all'on_picked 收到 路径列表on_error 收到结构化错误。媒体使用文件式传输,选择大视频时不会先把完整文件读入内存。

python
import appui

state = appui.State(paths=[])


def on_picked(paths):
    state.paths = paths or []


def on_pick_error(error):
    state.paths = [error.get("message", "选择失败")]


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,
            on_error=on_pick_error,
            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

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

文件选择由系统界面完成。默认 copy=True 会在安全访问范围内复制到会话临时目录,最适合普通 MiniApp;copy=False 只在视图存活期间保持外部文件授权。需要读取文件内容时,在回调里保存路径,再在按钮动作、刷新函数或后台流程中读取,避免在 body() 里同步读大文件。


#CameraPicker:相机

source'camera''rear''back''front'media_type'photo''video'on_captured 收到单个路径字符串;取消走 on_cancel,失败走 on_error。照片可配置 qualityallows_editing;视频可配置 video_qualitymaximum_duration

python
import appui

state = appui.State(last="")


def on_captured(path):
    state.last = path or ""


def on_cancel():
    state.last = "已取消"


def on_error(error):
    state.last = error.get("message", "相机不可用")


def body():
    return appui.VStack([
        appui.Text(state.last or "尚未拍摄").font("caption"),
        appui.CameraPicker(
            source="camera",
            media_type="photo",
            on_captured=on_captured,
            on_cancel=on_cancel,
            on_error=on_error,
            quality=0.9,
            label=appui.Label("拍照", system_image="camera.fill"),
        ),
    ], spacing=12).padding()

appui.run(body, state=state, presentation="sheet")

#VideoPlayer:AVKit

url 可为远程 HTTPS 或应用内可访问的本地文件名。autoplayloopshow_controls 控制播放行为。

只展示视频时直接用 VideoPlayer(url=...)。如果页面需要播放/暂停/seek、保存进度、倍速、PiP 状态或切集,使用 PlayerController 并传给 VideoPlayer(player=player);AppUI 新页面不要再 import avplayer 控制同一块内嵌视频。

python
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

WebView 支持双向导航状态、前进/后退/刷新/停止、JavaScript 执行、网页消息、Cookie/网站数据命令、原生下载、临时数据仓库和 host allowlist。命令必须带稳定 id,这样 UI 重建不会重复执行同一命令。

python
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)。声明了 miniapp.json capabilities 的 MiniApp 还需要 network 与匹配的 allowedNetworkHosts;控件自己的 allowed_hosts 只能进一步缩小范围。两者都限制为最多 128 条、UTF-8 总计最多 32768 字节,超限或含非法规则时安全拒绝。运行时会再次按 MiniApp 的不可变权限清单核验页面声明,页面节点不能自行扩权;同一有效集合用于导航、HTTPS/WSS 子资源、Cookie 和网站数据,其他分层网络 scheme 默认阻断。权限快速变化时,每个 WebView 只保留一个正在编译和一个最新待编译策略,全局编译队列也有界。

若页面来自当前 MiniApp 的原生 http_server,使用 http_server.start(port=0, url_mode="browser") 返回的地址。宿主只对当前运行的精确随机 localhost origin 开临时授权,因而根相对 CSS/JS/图片可以正常加载;无需也不应把动态 hostname 写入 allowedNetworkHosts。server 停止、替换或 MiniApp 会话结束后,该授权和连接同时失效。默认 url_mode="universal" 更适合 URLSession/普通客户端,但其令牌位于 path 中,不适合作为含 /style.css 的网页根。

本地内容可以传绝对路径或 file:// URL。声明式 MiniApp 读取自身工作区时需要 file_system;系统分享或外部启动交付给当前运行的 LaunchItem.path 属于 host-issued 临时只读授权,不需要额外声明 file_system,但不能写入、不能跨运行保存授权,也不能借此访问工作区或授权根以外的路径。WebView(url=本地文件) 会先规范化、确认文件真实存在且位于当前运行的可读根,再由原生 loadFileURL 限制读取范围。内联 HTML 可用 WebView(html=..., base_url=本地目录或文件) 加载同一授权根内的 CSS、JS 与图片,授权根目录本身也可作为 base_url。只有创建时已进入本地模式的 WebView 才能用 load_url 切换本地文件,且目标仍须位于同一原生范围;远程页面不会继承文件权限,本地页面的主 frame(包括 target=_blank 被折叠到当前视图的导航)也不能跳到远程页面。若页面依赖根相对 URL、ES module、fetch 或前端路由,仍推荐上面的 http_server browser URL,以获得完整 HTTP origin 语义。

WebView 的权限固定归属创建它的 AppUI 运行会话。Tab/页面重建和合法 generation 切换不会误撤权;MiniApp 停止、宿主任务取消或会话结束时,页面会停止并清空,下载与回调资源一并取消,旧 WebView 不会借下一次运行恢复。

需要下载时同时提供 allows_downloads=True 和命名的 on_download(path) 回调,并声明 file_system。返回路径是当前运行会话拥有的临时文件:UI 重建不会令它失效,MiniApp 停止后会自动删除。Cookie 与网站数据命令为 get_cookiesset_cookiedelete_cookieclear_website_data;每条命令都必须有新的稳定 id


#MapView:地图与标注

默认中心为旧金山坐标。span 为经纬跨度。map_style 可为 'automatic''standard''satellite''imagery''hybrid'。生产页面优先用 region=state.bind.region 双向同步原生相机;标记提供稳定 id,轨迹与区域使用 polyline / polygon / circle overlays。

python
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;同一应用内可对照理解差异:

python
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流媒体或本地视频
WebViewurlhtml 嵌入网页
MapView坐标、跨度、标注与样式

更完整的参数说明见同目录下的 appui-ref-media.md