实时活动(灵动岛)

LiveActivity API 允许你的脚本在 iOS 的锁屏界面以及支持的设备上的动态岛中展示实时数据。通过该 API,你可以创建、更新并结束 Live Activity,同时监听其生命周期状态和系统支持情况。

本文件详细介绍 Scripting app 中的 LiveActivity API,包括:

  • Live Activity 的生命周期与核心概念
  • 如何注册 Live Activity UI
  • 如何在脚本中启动、更新、结束 Live Activity
  • 如何构建 Live Activity UI(包括 Dynamic Island 多种布局)
  • 所有类型参数说明
  • 完整示例代码与最佳实践

本 API 基于 Apple ActivityKit 能力,并以 TypeScript/TSX 的方式封装,允许开发者使用 React 风格构建 Lock Screen 与 Dynamic Island 界面。


1. Live Activity 概念理解

Live Activity 展示在以下区域:

  • 锁屏界面
  • iPhone 14 Pro+ 的 Dynamic Island
  • 其他设备的悬浮样式(Banner)

它能随着应用或脚本运行实时更新内容,如:

  • 计时器
  • 外卖进度
  • 健身、运动状态
  • 倒计时、打卡、提醒

在 Scripting app 中,一个 Live Activity 由两部分组成:

  1. 内容状态(contentState) 一个 JSON 可序列化的对象,会随时间改变。
  2. UI Builder 通过 TSX 描述不同区域的展示方式。

2. Live Activity 状态类型

1type LiveActivityState = "active" | "dismissed" | "ended" | "stale";
状态 描述
active 正在显示,可以更新内容
stale 已过期,需要更新 staleDate 后才能恢复 active
ended 活动已结束但仍在锁屏显示(最长 4 小时或自定时间)
dismissed 已被系统或用户移除,不再可见

3. LiveActivityDetail 类型

1type LiveActivityDetail = {
2  id: string;
3  state: LiveActivityState;
4};

用于描述当前正在运行的所有 Live Activity 信息。


4. LiveActivity UI 构建类型

4.1 LiveActivityUIProps

1type LiveActivityUIProps = {
2  content: VirtualNode;
3  compactLeading: VirtualNode;
4  compactTrailing: VirtualNode;
5  minimal: VirtualNode;
6  children: VirtualNode | VirtualNode[];
7};

这些字段对应 Dynamic Island:

  • content:锁屏和普通设备顶部 Banner 显示
  • compactLeading / compactTrailing:Dynamic Island 收缩状态左右区域
  • minimal:最小化的单点显示
  • children:展开后的多个区域(使用 LiveActivityUIExpanded* 包裹)

5. 注册 Live Activity UI

Live Activity 必须放在单独的文件中,例如 live_activity.tsx

1import { LiveActivity, LiveActivityUI, LiveActivityUIBuilder } from "scripting";
2
3export type State = {
4  mins: number;
5};
6
7function ContentView(state: State) {
8  return (
9    <HStack activityBackgroundTint={{ light: "clear", dark: "clear" }}>
10      <Image systemName="waterbottle" foregroundStyle="systemBlue" />
11      <Text>{state.mins}分钟后补水</Text>
12    </HStack>
13  );
14}
15
16const builder: LiveActivityUIBuilder<State> = (state) => {
17  return (
18    <LiveActivityUI
19      content={<ContentView {...state} />}
20      compactLeading={
21        <HStack>
22          <Image systemName="clock" />
23          <Text>{state.mins}m</Text>
24        </HStack>
25      }
26      compactTrailing={<Image systemName="waterbottle" foregroundStyle="systemBlue" />}
27      minimal={<Image systemName="clock" />}>
28      <LiveActivityUIExpandedCenter>
29        <ContentView {...state} />
30      </LiveActivityUIExpandedCenter>
31    </LiveActivityUI>
32  );
33};
34
35export const MyLiveActivity = LiveActivity.register("MyLiveActivity", builder);

6. 在脚本中使用 Live Activity

下面展示如何启动、更新、监听状态并结束 Live Activity。

1import {
2  Button,
3  Text,
4  VStack,
5  Navigation,
6  NavigationStack,
7  useMemo,
8  useState,
9  LiveActivityState,
10  BackgroundKeeper,
11} from "scripting";
12import { MyLiveActivity } from "./live_activity";
13
14function Example() {
15  const dismiss = Navigation.useDismiss();
16  const [state, setState] = useState<LiveActivityState>();
17
18  const activity = useMemo(() => {
19    const instance = MyLiveActivity();
20
21    instance.addUpdateListener((s) => {
22      setState(s);
23      if (s === "dismissed") {
24        BackgroundKeeper.stop();
25      }
26    });
27
28    return instance;
29  }, []);
30
31  return (
32    <NavigationStack>
33      <VStack
34        navigationTitle="LiveActivity 示例"
35        navigationBarTitleDisplayMode="inline"
36        toolbar={{
37          cancellationAction: <Button title="完成" action={dismiss} />,
38        }}>
39        <Text>当前状态:{state ?? "-"}</Text>
40
41        <Button
42          title="启动 Live Activity"
43          disabled={state != null}
44          action={() => {
45            let count = 5;
46            BackgroundKeeper.keepAlive();
47
48            activity.start({ mins: count });
49
50            function tick() {
51              setTimeout(() => {
52                count -= 1;
53
54                if (count === 0) {
55                  activity.end({ mins: 0 });
56                  BackgroundKeeper.stop();
57                } else {
58                  activity.update({ mins: count });
59                  tick();
60                }
61              }, 60000);
62            }
63            tick();
64          }}
65        />
66      </VStack>
67    </NavigationStack>
68  );
69}
70
71async function run() {
72  await Navigation.present(<Example />);
73  Script.exit();
74}
75
76run();

7. LiveActivity 类 API 说明

7.1 start(contentState, options?)

1start(contentState: T, options?: LiveActivityOptions): Promise<boolean>
  • 请求系统启动 Live Activity
  • contentState 必须可以 JSON 序列化

LiveActivityOptions

1type LiveActivityOptions = {
2  staleDate?: number | Date;
3  relevanceScore?: number;
4};
  • staleDate:到期变为 stale 的时间戳(ms) 或 Date 对象
  • relevanceScore:控制 Dynamic Island 的优先级

7.2 update(contentState, options?)

1update(contentState: T, options?: LiveActivityUpdateOptions)

LiveActivityUpdateOptions

1type LiveActivityUpdateOptions = {
2  staleDate?: number | Date;
3  relevanceScore?: number;
4  alert?: {
5    title: string;
6    body: string;
7  };
8};

可带 Apple Watch 的更新提示。


7.3 end(contentState, options?)

1end(contentState: T, options?: LiveActivityEndOptions)

LiveActivityEndOptions

1type LiveActivityEndOptions = {
2  staleDate?: number | Date;
3  relevanceScore?: number;
4  dismissTimeInterval?: number;
5};

dismissTimeInterval(单位秒):

  • 未提供:系统默认最长保留 4 小时
  • <= 0:立即移除
  • > 0:指定多久后移除

7.4 获取活动状态

1getActivityState(): Promise<LiveActivityState | null>

7.5 监听状态更新

1addUpdateListener(listener);
2removeUpdateListener(listener);

当 Live Activity 状态变更时回调,例如:

  • active → stale
  • active → ended
  • ended → dismissed

7.6 静态方法

1static areActivitiesEnabled(): Promise<boolean>
2static getAllActivities(): Promise<LiveActivityDetail[]>
3static getAllActivitiesIds(): Promise<string[]>
4static getActivityState(activityId: string)
5static from(activityId, name)
6static endAllActivities(options?)

8. Live Activity UI 组件

组件 描述
LiveActivityUI 注册 UI 的根结构
LiveActivityUIExpandedCenter 展开状态的中间区域
LiveActivityUIExpandedLeading 左侧区域
LiveActivityUIExpandedTrailing 右侧区域
LiveActivityUIExpandedBottom 底部区域

用于构建 Dynamic Island 展开布局。


9. 注意事项与最佳实践

9.1 必须 JSON 可序列化

contentState 中不能包含:

  • 函数
  • Date 对象(需转 timestamp)
  • class 实例
  • 非可序列化对象

9.2 Live Activity 必须放在独立文件

例如:

live_activity.tsx

这与系统对 UI 构建的要求有关。

9.3 Scripting 的 Live Activity 与脚本生命周期隔离

即使脚本结束,Live Activity 会继续保持。

若你希望脚本保持运行,可使用:

1BackgroundKeeper.keepAlive();

10. 完整示例(简化版)

1const activity = MyLiveActivity();
2
3await activity.start({ mins: 10 });
4
5await activity.update({ mins: 5 });
6
7await activity.end({ mins: 0 }, { dismissTimeInterval: 0 });

11. 注意事项

  • Live Activity 的启动是异步的,需要等到 start 返回 true 时才能调用 updateend
  • Live Activity 不能访问 Documents 和 iCloud 目录,只能访问 app group 目录,如果你想要访问文件或者渲染图片,必须把文件或图片保存到 FileManager.appGroupDocumentsDirectory 目录中。 比如渲染图片,你保存到 FileManager.appGroupDocumentsDirectory 中, 再通过 <Image filePath={Path.join(FileManager.appGroupDocumentsDirectory, 'example.png')} /> 渲染
  • Live Activity 可以访问与 App 共享的 Storage 数据