PythonIDE Docs
中文
简体中文

媒体 API

Image、AsyncImage、PhotoPicker、CameraPicker、VideoPlayer、WebView 和 MapView。

本页覆盖图片、相册、相机、文件导入、地图、网页、视频和图标标题。媒体视图仍然是普通 View,可以继续使用 .frame(...).padding(...).background(...).clipped() 等修饰符。

#什么时候用

目标首选 API说明
SF Symbol 或本地图片Image图标、资产目录图片、可缩放图片。
网络图片AsyncImage支持占位视图和失败视图。
图标 + 标题Label按钮、列表行、Tab、菜单项里的标准图标标题。
从相册选择PhotoPicker选择图片或视频,回调返回路径列表。
拍照或录像CameraPicker使用系统相机,回调返回路径字符串。
从文件导入FileImporter打开系统文件选择器,回调返回导入文件路径列表。
视频播放VideoPlayer内联或全屏视频播放,支持 AirPlay、PiP。
播放器控制PlayerController复用同一个原生播放器实例,控制播放、暂停、进度、倍速、音量和事件回调。
网页内容WebView加载 URL 或内联 HTML。
地图展示MapView显示 Apple Maps、中心点、缩放跨度和标记。

#最小正确示例

python
import appui

state = appui.State(status="Ready", picked_count=0, imported_count=0)


def image_loaded():
    state.status = "Image loaded"


def image_failed():
    state.status = "Image failed"


def receive_photos(paths):
    state.picked_count = len(paths or [])
    state.status = f"Picked {state.picked_count} item(s)"


def receive_files(paths):
    state.imported_count = len(paths or [])
    state.status = f"Imported {state.imported_count} file(s)"


def body():
    return appui.NavigationStack(
        appui.List([
            appui.Section("Image", [
                appui.AsyncImage(
                    url="https://www.w3.org/Icons/w3c_home",
                    placeholder=appui.ProgressView(label="Loading"),
                    error_view=appui.Label("Failed", system_image="wifi.slash"),
                    content_mode="fit",
                    on_success=image_loaded,
                    on_failure=image_failed,
                )
                .frame(height=90)
                .background("secondarySystemBackground", corner_radius=8),
            ]),
            appui.Section("Picker", [
                appui.PhotoPicker(
                    selection_limit=2,
                    filter="images",
                    on_picked=receive_photos,
                    label=appui.Label("Choose Photos", system_image="photo.on.rectangle"),
                ),
                appui.Text(state.status).font("footnote").foreground_color("secondaryLabel"),
            ]),
            appui.Section("Files", [
                appui.FileImporter(
                    allowed_types=["text", "pdf", "csv"],
                    allows_multiple=True,
                    on_picked=receive_files,
                    label=appui.Label("Import Files", system_image="doc.badge.plus"),
                ),
            ]),
        ]).navigation_title("Media")
    )


appui.run(body, state=state)

#图片和标签

API签名分类
ImageImage(name: Optional[str] = None, system_name: Optional[str] = None, systemName: Optional[str] = None)media
LabelLabel(title: str = '', system_image: Optional[str] = None, image: Optional[str] = None, systemImage: Optional[str] = None)text
AsyncImageAsyncImage(url: str = '', placeholder: Optional[View] = None, error_view: Optional[View] = None, content_mode: str = 'fit', on_success: Optional[Callable] = None, on_failure: Optional[Callable] = None)media

#Image 方法

方法说明
.resizable()允许图片按 frame 缩放。
.aspect_ratio(ratio=None, content_mode='fit', **kwargs)设置宽高比和填充模式;content_mode"fit""fill"
.symbol_rendering_mode(mode)SF Symbol 渲染:"hierarchical""palette""multicolor""monochrome"
.image_scale(scale)SF Symbol 尺寸:"small""medium""large"
python
import appui


def body():
    symbol = (
        appui.Image(system_name="heart.fill")
        .symbol_rendering_mode("multicolor")
        .image_scale("large")
        .foreground_color("systemPink")
    )

    photo = (
        appui.Image(name="example")
        .resizable()
        .aspect_ratio(content_mode="fit")
        .frame(height=90)
        .background("secondarySystemBackground", corner_radius=8)
    )

    return appui.NavigationStack(
        appui.VStack([symbol, photo], spacing=16)
        .padding()
        .navigation_title("Images")
    )


appui.run(body)

#相册、相机和文件导入

API签名分类
PhotoPickerPhotoPicker(selection_limit: int = 1, filter: str = 'images', on_picked: Optional[Callable] = None, label: Optional[View] = None, selectionLimit: Optional[int] = None, onPicked: Optional[Callable] = None, **kwargs: Any)media
CameraPickerCameraPicker(source: str = 'camera', media_type: str = 'photo', on_captured: Optional[Callable] = None, label: Optional[View] = None, mediaType: Optional[str] = None, onCaptured: Optional[Callable] = None, **kwargs: Any)media
FileImporterFileImporter(allowed_types: Optional[Union[str, Sequence[str]]] = None, allows_multiple: bool = False, copy: bool = True, on_picked: Optional[Callable] = None, label: Optional[View] = None, allowedTypes: Optional[Union[str, Sequence[str]]] = None, allowsMultiple: Optional[bool] = None, onPicked: Optional[Callable] = None, **kwargs: Any)media

回调契约:PhotoPicker.on_picked(paths)FileImporter.on_picked(paths) 接收路径字符串列表;CameraPicker.on_captured(path) 接收单个路径字符串或空值。用户可能拒绝权限、取消选择或设备不可用,回调中要处理空路径和空列表。

python
import appui

state = appui.State(last_path="")


def receive_capture(path):
    state.last_path = path or "No file"


def body():
    return appui.NavigationStack(
        appui.Form([
            appui.Section("Camera", [
                appui.CameraPicker(
                    source="front",
                    media_type="photo",
                    on_captured=receive_capture,
                    label=appui.Label("Take Photo", system_image="camera"),
                ),
                appui.Text(state.last_path or "No capture yet")
                    .font("footnote")
                    .foreground_color("secondaryLabel"),
            ])
        ]).navigation_title("Camera")
    )


appui.run(body, state=state)
python
import appui

state = appui.State(files=[])


def receive_files(paths):
    state.files = paths or []


def body():
    rows = [
        appui.Text(path).font("footnote").line_limit(1)
        for path in state.files
    ]
    return appui.NavigationStack(
        appui.Form([
            appui.Section("Import", [
                appui.FileImporter(
                    allowed_types=["text", "pdf", "csv"],
                    allows_multiple=True,
                    on_picked=receive_files,
                    label=appui.Label("Import Files", system_image="folder"),
                ),
            ]),
            appui.Section("Files", rows or [
                appui.ContentUnavailableView(
                    "No file",
                    system_image="doc",
                    description="Import a document first",
                )
            ]),
        ]).navigation_title("Files")
    )


appui.run(body, state=state)

#视频、网页和地图

#VideoPlayer {#video-player}

#PlayerController {#player-controller}

#WebView {#webview}

#视频 API 选择规则

场景推荐写法说明
只需要在页面里显示并播放一个视频VideoPlayer(url=...)最简单,适合普通预览、详情页视频。
Mini App 需要播放/暂停/seek/倍速/进度保存/PiP 状态PlayerController + VideoPlayer(player=player)AppUI 视频类应用的主入口。
不写 AppUI 页面,只写脚本播放音视频import avplayer脚本级媒体能力;AppUI 新页面不要优先用它控制内嵌播放器。
API签名分类
VideoPlayerVideoPlayer(url: str = '', autoplay: bool = False, loop: bool = False, show_controls: bool = True, presentation: str = 'inline', allows_fullscreen: bool = True, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', enters_fullscreen_when_playback_begins: bool = False, exits_fullscreen_when_playback_ends: bool = True, showControls: Optional[bool] = None, allowsFullscreen: Optional[bool] = None, allowsPiP: Optional[bool] = None, allowsPictureInPicture: Optional[bool] = None, allowsAirPlay: Optional[bool] = None, videoGravity: Optional[str] = None, entersFullscreenWhenPlaybackBegins: Optional[bool] = None, exitsFullscreenWhenPlaybackEnds: Optional[bool] = None, allows_picture_in_picture: Optional[bool] = None, player: Optional[PlayerController] = None, player_id: Optional[str] = None, pause_on_disappear: Optional[bool] = None)media
PlayerControllerPlayerController(id: str = 'main', url: str = '', autoplay: bool = False, loop: bool = False, rate: float = 1.0, volume: float = 1.0, allows_pip: bool = True, allows_airplay: bool = True, video_gravity: str = 'resizeAspect', pause_on_disappear: bool = True)公开类型
WebViewWebView(url: Optional[str] = None, html: Optional[str] = None)media
MapViewMapView(latitude: float = 37.7749, longitude: float = -122.4194, span: float = 0.05, markers: Optional[Sequence[Dict[str, Any]]] = None, map_style: str = 'automatic', mapStyle: Optional[str] = None)media
ShareLinkShareLink(item: str = '', subject: Optional[str] = None, message: Optional[str] = None)control

VideoPlayer(url=...) 适合只展示并播放一个视频。视频类 Mini App 需要恢复播放进度、切集、外部按钮控制、倍速、音量或 PiP 状态时,先创建 PlayerController,再传给 VideoPlayer(player=player)。AppUI 页面不要再额外 import avplayer 去控制同一块内嵌视频;PlayerController 已经是 AppUI 的播放器控制入口。

PlayerController 默认 pause_on_disappear=True,页面退出或视图消失时会暂停对应播放器,避免视频声音继续播放。确实需要离开页面后继续播放时,显式传 pause_on_disappear=False

python
import appui

player = appui.PlayerController(
    id="episode-player",
    url="https://example.com/video.mp4",
    autoplay=True,
    allows_pip=True,
    pause_on_disappear=True,
)


@player.on_progress(interval=5)
def save_progress(seconds):
    print("progress", seconds)


def skip_forward():
    player.seek(player.current_time + 30)


def body():
    return appui.NavigationStack(
        appui.VStack([
            appui.VideoPlayer(player=player).frame(height=220),
            appui.HStack([
                appui.Button("Play", action=player.play),
                appui.Button("Pause", action=player.pause),
                appui.Button("Skip", action=skip_forward),
            ]),
        ], spacing=12)
        .padding()
        .navigation_title("Player")
    )


appui.run(body)
python
import appui


def body():
    markers = [
        {"latitude": 35.68, "longitude": 139.76, "title": "Tokyo"},
        {"latitude": 35.69, "longitude": 139.70, "title": "Shinjuku"},
    ]

    return appui.NavigationStack(
        appui.ScrollView(
            appui.VStack([
                appui.MapView(
                    latitude=35.68,
                    longitude=139.76,
                    span=0.12,
                    markers=markers,
                    map_style="standard",
                ).frame(height=220),
                appui.WebView(html="<h1>AppUI</h1><p>Inline HTML content.</p>")
                    .frame(height=180),
            ], spacing=16)
            .padding()
        ).navigation_title("Map & Web")
    )


appui.run(body)

#加载状态和权限

API空值或失败时的表现建议
AsyncImage无占位时可能显示空白;失败时使用 error_view总是提供 placeholdererror_view
PhotoPicker用户取消时可能返回空列表。on_picked 中处理 []None
CameraPicker用户拒绝权限、取消或设备不可用时可能没有路径。on_captured 中处理空路径并显示说明。
FileImporter用户取消时不会产生有效路径;部分外部文件类型可能无法读取。默认保持 copy=True,在 on_picked 中处理 []None
VideoPlayerurl 没有有效播放源。文档示例可展示空控件,真实应用必须传可播放地址。
WebView未传 url / html 时没有明确内容。显式传 URL 或 HTML。
MapView无标记也能显示中心点。span 不要过大,标记字典至少包含 latitudelongitude
ShareLinkitem 为空时分享面板没有有意义内容。先在状态或函数里生成可读文本、URL 或文件路径。

#与相邻 API 的区别

API不同点
Image vs LabelImage 只有图像;Label 是图标和标题组合,按钮和列表行更常用。
Image vs AsyncImageImage 用本地资源或 SF Symbol;AsyncImage 从网络 URL 加载。
PhotoPicker vs CameraPickerPhotoPicker 从已有媒体库选择;CameraPicker 创建新媒体。
PhotoPicker vs FileImporterPhotoPicker 只面向相册媒体;FileImporter 从 Files App 或文档提供方导入普通文件。
WebView vs LinkWebView 在 AppUI 内嵌网页;Link 打开系统浏览器。
VideoPlayer vs WebView视频用 VideoPlayer,不要用 WebView 包一层视频网页来播放。

#常见错误

错误正确做法
AsyncImage 不设固定高度,加载后布局跳动。.frame(height=...) 固定媒体区域。
网络图片使用 "fill" 但忘记 .clipped()填充裁切时加 .clipped()
相册或相机回调假设一定有路径。对空列表、空字符串和权限拒绝做处理。
需要普通文档却用 PhotoPickerFileImporter(allowed_types=[...], on_picked=...)
Image(...).on_tap(...) 模拟按钮。可点击媒体动作使用 Button(content=appui.Image(...))Button(content=appui.Label(...))
在用户文档里混用命令式 ui 写法。AppUI 文档只展示声明式 body() + appui.run(...)

#相关文档

文档用途
控件 APIButton、Label、Picker 和输入控件。
图表与画布 APIChart、Canvas、DrawingContext、Path。
性能指南媒体和大数据刷新时的性能边界。