跳转至

Widget 小组件

Widget 模块提供了 Scriptable 兼容的小组件构建能力。你可以使用 JavaScript 构建 iOS 主屏幕小组件的界面,支持文本、图片、日期、堆栈布局、渐变背景等丰富的 UI 元素。

Scriptable 兼容: API 与 Scriptable 保持一致,已有的 Scriptable Widget 脚本可直接运行。

守护进程支持: 完全支持。脚本构建的 Widget 树会序列化并保存到 App Group,由 Widget Extension 渲染。


目录


快速开始

创建一个简单的 Widget 只需要三步:

// 1. 创建 Widget
const widget = new ListWidget();
widget.backgroundColor = Color.blue();

// 2. 添加内容
const title = widget.addText('Hello Widget!');
title.textColor = Color.white();
title.font = Font.boldSystemFont(20);

// 3. 设置为小组件
Script.setWidget(widget);

工作原理

JS 脚本 → new ListWidget() → 构建节点树 → Script.setWidget()
→ 序列化为 JSON → 保存到 App Group → Widget Extension 读取并渲染
  1. JS 脚本通过 new ListWidget() 创建根节点
  2. 通过 addText()addImage()addStack() 等方法构建节点树
  3. 调用 Script.setWidget(widget) 将节点树序列化并保存
  4. iOS Widget Extension 从 App Group 读取数据并用 SwiftUI 渲染

API 参考

ListWidget 根容器

new ListWidget()

创建一个 Widget 根容器。所有小组件都以 ListWidget 作为根节点。

const widget = new ListWidget();

属性:

属性 类型 说明
backgroundColor Color 背景颜色
backgroundImagePath string 背景图片路径(本地文件路径)
backgroundGradient LinearGradient 背景渐变
spacing number 子元素间距(points)
url string 点击 Widget 时打开的 URL
refreshAfterDate number 下次刷新时间戳(Unix timestamp,秒)
const widget = new ListWidget();
widget.backgroundColor = Color.black();
widget.spacing = 8;
widget.url = 'https://example.com';

// 30 分钟后刷新
widget.refreshAfterDate = Date.now() / 1000 + 1800;

方法:

方法 返回 说明
addText(text) WidgetText 添加文本
addImage(img) WidgetImage 添加图片
addDate(date) WidgetDate 添加日期
addSpacer(length?) WidgetSpacer 添加间距
addStack() WidgetStack 添加堆栈容器
setPadding(top, leading, bottom, trailing) - 设置内边距
useDefaultPadding() - 恢复默认内边距

WidgetStack 堆栈容器

堆栈容器用于组织子元素的布局方向(水平或垂直)。

const stack = widget.addStack();
stack.layoutHorizontally();
stack.spacing = 8;

属性:

属性 类型 说明
backgroundColor Color 背景颜色
backgroundImagePath string 背景图片路径
backgroundGradient LinearGradient 背景渐变
spacing number 子元素间距
size Size 堆栈尺寸
opacity number 透明度(0.0 ~ 1.0)
cornerRadius number 圆角半径
borderWidth number 边框宽度
borderColor Color 边框颜色
shadowColor Color 阴影颜色
shadowRadius number 阴影半径
shadowOffset Point 阴影偏移
url string 点击时打开的 URL

布局方法:

方法 说明
layoutHorizontally() 水平排列子元素
layoutVertically() 垂直排列子元素(默认)
topAlignContent() 子元素顶部对齐
centerAlignContent() 子元素居中对齐
bottomAlignContent() 子元素底部对齐

子元素方法: 同 ListWidget(addTextaddImageaddDateaddSpaceraddStacksetPaddinguseDefaultPadding)。

// 水平布局示例
const row = widget.addStack();
row.layoutHorizontally();
row.centerAlignContent();
row.spacing = 12;

row.addImage(SFSymbol.named('sun.max.fill'));
row.addText('晴天');
row.addSpacer();
row.addText('28°C');

WidgetText 文本元素

addText() 返回的文本元素对象。

const text = widget.addText('Hello');
text.textColor = Color.white();
text.font = Font.boldSystemFont(18);

属性:

属性 类型 说明
text string 文本内容
textColor Color 文本颜色
font Font 字体
textOpacity number 透明度(0.0 ~ 1.0)
lineLimit number 最大行数(0 = 不限)
minimumScaleFactor number 最小缩放比例(0.0 ~ 1.0)
shadowColor Color 阴影颜色
shadowRadius number 阴影半径
shadowOffset Point 阴影偏移
url string 点击时打开的 URL

对齐方法:

方法 说明
leftAlignText() 左对齐
centerAlignText() 居中对齐
rightAlignText() 右对齐

WidgetImage 图片元素

addImage() 返回的图片元素对象。

// SF Symbol 图标
const icon = widget.addImage(SFSymbol.named('star.fill'));
icon.tintColor = Color.yellow();
icon.imageSize = new Size(24, 24);

// 本地文件
const photo = widget.addImage({ path: '/path/to/image.png' });

// Base64
const avatar = widget.addImage({ base64: 'iVBORw0KGgo...' });

addImage 参数:

参数类型 说明
string SF Symbol 名称
SFSymbol SF Symbol 对象
{ path } 本地文件路径
{ base64 } Base64 编码图片
{ sfSymbolName } SF Symbol 名称

属性:

属性 类型 说明
image object 图片数据
resizable boolean 是否可缩放
imageSize Size 图片尺寸
imageOpacity number 透明度(0.0 ~ 1.0)
cornerRadius number 圆角半径
borderWidth number 边框宽度
borderColor Color 边框颜色
shadowColor Color 阴影颜色
shadowRadius number 阴影半径
shadowOffset Point 阴影偏移
containerRelativeShape boolean 是否跟随容器形状
tintColor Color 着色(适用于 SF Symbol)
url string 点击时打开的 URL

方法:

方法 说明
leftAlignImage() 左对齐
centerAlignImage() 居中对齐
rightAlignImage() 右对齐
applyFittingContentMode() 适应模式(保持比例)
applyFillingContentMode() 填充模式

WidgetDate 日期元素

显示日期/时间,支持多种格式样式。由 addDate() 返回。

const dateEl = widget.addDate(new Date());
dateEl.applyTimeStyle();
dateEl.font = Font.mediumSystemFont(14);

属性: 同 WidgetText(textColorfonttextOpacitylineLimitminimumScaleFactorshadowColorshadowRadiusshadowOffseturl)。

日期样式方法:

方法 显示效果示例
applyTimeStyle() 3:45 PM
applyDateStyle() March 10, 2026
applyRelativeStyle() 2 hours ago
applyOffsetStyle() +2 hours
applyTimerStyle() 2:30:00(倒计时)

对齐方法: leftAlignText()centerAlignText()rightAlignText()


WidgetSpacer 间距元素

在容器中添加弹性或固定间距。由 addSpacer() 返回。

// 弹性间距(自动填充剩余空间)
widget.addSpacer();

// 固定间距
widget.addSpacer(20);

属性:

属性 类型 说明
length number 间距长度(不设则为弹性间距)

Script 脚本工具

Script.setWidget(widget)

将构建好的 Widget 树保存为小组件数据。

const widget = new ListWidget();
widget.addText('Done!');
Script.setWidget(widget);

调用后 Widget 节点树被序列化为 JSON 并保存到 App Group,Widget Extension 会在下次刷新时读取并渲染。

Script.complete()

通知脚本执行完毕(兼容 Scriptable,在 TrollScript 中为空操作)。

Script.setWidget(widget);
Script.complete();

样式类

Color 颜色

new Color(hex, alpha)

创建自定义颜色。

参数 类型 说明
hex string 颜色值(如 '#FF0000''FF0000''F00'
alpha number 透明度 0.0 ~ 1.0,默认 1.0
const red = new Color('#FF0000');
const semiBlue = new Color('#0000FF', 0.5);

只读属性: hexredgreenbluealpha

静态方法(预设颜色):

方法 颜色
Color.black() 黑色
Color.white() 白色
Color.red() 红色
Color.green() 绿色
Color.blue() 蓝色
Color.yellow() 黄色
Color.orange() 橙色
Color.purple() 紫色
Color.cyan() 青色
Color.magenta() 洋红
Color.brown() 棕色
Color.gray() 灰色
Color.darkGray() 深灰
Color.lightGray() 浅灰
Color.clear() 透明
// 动态颜色(浅色/深色模式)
const textColor = Color.dynamic(Color.black(), Color.white());

Font 字体

语义字体(自动适配系统设置):

方法 说明
Font.largeTitle() 大标题
Font.title1() 标题 1
Font.title2() 标题 2
Font.title3() 标题 3
Font.headline() 标题行
Font.subheadline() 副标题行
Font.body() 正文
Font.callout() 标注
Font.footnote() 脚注
Font.caption1() 说明 1
Font.caption2() 说明 2

系统字体(指定大小和粗细):

Font.regularSystemFont(16)    // 常规
Font.mediumSystemFont(16)     // 中等
Font.semiboldSystemFont(16)   // 半粗
Font.boldSystemFont(16)       // 粗体
Font.heavySystemFont(16)      // 重型
Font.blackSystemFont(16)      // 极粗
Font.lightSystemFont(16)      // 轻型
Font.thinSystemFont(16)       // 纤细
Font.ultraLightSystemFont(16) // 极细
Font.italicSystemFont(16)     // 斜体

圆角系统字体:SystemFont 替换为 RoundedSystemFont

Font.boldRoundedSystemFont(20) // 粗体圆角

等宽系统字体:SystemFont 替换为 MonospacedSystemFont

Font.regularMonospacedSystemFont(14) // 等宽常规

自定义字体:

const customFont = new Font('Helvetica-Bold', 18);

LinearGradient 渐变

const gradient = new LinearGradient();
gradient.colors = [Color.blue(), Color.purple()];
gradient.locations = [0, 1];
gradient.startPoint = new Point(0, 0);  // 左上
gradient.endPoint = new Point(1, 1);    // 右下

widget.backgroundGradient = gradient;

属性:

属性 类型 说明
colors Color[] 渐变颜色数组
locations number[] 每个颜色的位置(0.0 ~ 1.0)
startPoint Point 渐变起点
endPoint Point 渐变终点

Size 尺寸

const size = new Size(100, 50);
image.imageSize = size;

属性: widthheight


Point 坐标

const point = new Point(0.5, 0.5);
text.shadowOffset = point;

属性: xy


SFSymbol 系统图标

const symbol = SFSymbol.named('heart.fill');
const img = widget.addImage(symbol);
img.tintColor = Color.red();

SFSymbol.named(name)

创建系统图标引用。name 为 SF Symbols 图标名称。

图标名称参考:SF Symbols


完整示例

示例 1: 天气小组件

const widget = new ListWidget();

// 渐变背景
const gradient = new LinearGradient();
gradient.colors = [new Color('#4A90D9'), new Color('#357ABD')];
gradient.locations = [0, 1];
widget.backgroundGradient = gradient;

// 城市
const city = widget.addText('北京');
city.textColor = Color.white();
city.font = Font.mediumSystemFont(14);

widget.addSpacer();

// 温度
const temp = widget.addText('28°');
temp.textColor = Color.white();
temp.font = Font.lightSystemFont(42);

// 天气描述
const desc = widget.addText('晴 · 体感温度 30°');
desc.textColor = new Color('#FFFFFF', 0.8);
desc.font = Font.regularSystemFont(13);

Script.setWidget(widget);

示例 2: 待办事项

const widget = new ListWidget();
widget.backgroundColor = Color.white();
widget.setPadding(16, 16, 16, 16);

// 标题行
const header = widget.addStack();
header.layoutHorizontally();
header.centerAlignContent();

const icon = header.addImage(SFSymbol.named('checklist'));
icon.tintColor = Color.blue();
icon.imageSize = new Size(16, 16);
header.addSpacer(6);
const title = header.addText('今日待办');
title.font = Font.boldSystemFont(15);
title.textColor = Color.blue();
header.addSpacer();
header.addText('3');

widget.addSpacer(8);

// 待办列表
const todos = ['完成项目报告', '回复邮件', '预约会议'];
todos.forEach(function(item) {
  const row = widget.addStack();
  row.layoutHorizontally();
  row.centerAlignContent();
  row.spacing = 6;

  const dot = row.addImage(SFSymbol.named('circle'));
  dot.tintColor = Color.gray();
  dot.imageSize = new Size(12, 12);
  const label = row.addText(item);
  label.font = Font.regularSystemFont(13);
  label.lineLimit = 1;
});

widget.addSpacer();

Script.setWidget(widget);

示例 3: 倒计时

const widget = new ListWidget();
widget.backgroundColor = new Color('#1C1C1E');

const label = widget.addText('距离新年');
label.textColor = Color.lightGray();
label.font = Font.caption1();

widget.addSpacer(4);

// 目标日期
const target = new Date('2027-01-01T00:00:00');
const dateEl = widget.addDate(target);
dateEl.applyTimerStyle();
dateEl.textColor = Color.orange();
dateEl.font = Font.boldSystemFont(28);

widget.addSpacer();

const footer = widget.addText('2027');
footer.textColor = Color.gray();
footer.font = Font.footnote();
footer.rightAlignText();

// 每小时刷新
widget.refreshAfterDate = Date.now() / 1000 + 3600;

Script.setWidget(widget);

示例 4: 数据卡片

const widget = new ListWidget();
widget.setPadding(12, 16, 12, 16);

// 渐变背景
const bg = new LinearGradient();
bg.colors = [new Color('#667eea'), new Color('#764ba2')];
bg.locations = [0, 1];
widget.backgroundGradient = bg;

const title = widget.addText('步数统计');
title.textColor = Color.white();
title.font = Font.caption1();

widget.addSpacer();

const count = widget.addText('12,580');
count.textColor = Color.white();
count.font = Font.boldSystemFont(28);

const unit = widget.addText('步 · 目标 85%');
unit.textColor = new Color('#FFFFFF', 0.7);
unit.font = Font.regularSystemFont(12);

widget.addSpacer(4);

// 进度条(用 Stack 模拟)
const barBg = widget.addStack();
barBg.layoutHorizontally();
barBg.backgroundColor = new Color('#FFFFFF', 0.3);
barBg.cornerRadius = 3;
barBg.size = new Size(0, 6);

const barFill = barBg.addStack();
barFill.backgroundColor = Color.white();
barFill.cornerRadius = 3;
barFill.size = new Size(120, 6);

Script.setWidget(widget);

示例 5: 网络数据 Widget

// 从 API 获取数据
const resp = http.get('https://api.example.com/quote');
const data = JSON.parse(resp.body);

const widget = new ListWidget();
widget.backgroundColor = new Color('#FAFAFA');
widget.setPadding(16, 16, 16, 16);

const quoteIcon = widget.addImage(SFSymbol.named('quote.opening'));
quoteIcon.tintColor = Color.gray();
quoteIcon.imageSize = new Size(20, 16);

widget.addSpacer(8);

const quote = widget.addText(data.content || '生活就是不断前行');
quote.font = Font.regularSystemFont(15);
quote.textColor = new Color('#333333');

widget.addSpacer();

const author = widget.addText('— ' + (data.author || '佚名'));
author.font = Font.italicSystemFont(13);
author.textColor = Color.gray();
author.rightAlignText();

// 每 6 小时刷新
widget.refreshAfterDate = Date.now() / 1000 + 21600;

Script.setWidget(widget);

示例 6: 圆角 + 透明 + 阴影卡片

const widget = new ListWidget();
widget.backgroundColor = new Color('#101014');

const card = widget.addStack();
card.setPadding(12, 12, 12, 12);
card.backgroundColor = new Color('#1C1C1E');
card.opacity = 0.92;
card.cornerRadius = 12;
card.shadowColor = new Color('#000000', 0.35);
card.shadowRadius = 8;
card.shadowOffset = new Point(0, 3);

const icon = card.addImage(SFSymbol.named('star.fill'));
icon.imageSize = new Size(32, 32);
icon.tintColor = new Color('#FFD60A');
icon.cornerRadius = 8;
icon.shadowColor = new Color('#000000', 0.35);
icon.shadowRadius = 6;
icon.shadowOffset = new Point(0, 2);

card.addSpacer(8);
const title = card.addText('Hello Widget');
title.textColor = Color.white();

Script.setWidget(widget);

最佳实践

1. 始终调用 Script.setWidget

// 正确 - 保存 Widget 数据
const widget = new ListWidget();
widget.addText('Hello');
Script.setWidget(widget);

// 错误 - Widget 不会显示
const widget2 = new ListWidget();
widget2.addText('Hello');
// 忘记调用 Script.setWidget

2. 使用弹性 Spacer 布局

// 推荐 - 自适应布局
widget.addText('顶部');
widget.addSpacer();  // 弹性空间
widget.addText('底部');

// 不推荐 - 固定间距在不同尺寸 Widget 上可能溢出
widget.addText('顶部');
widget.addSpacer(200);
widget.addText('底部');

3. 限制文本行数

// 推荐 - 避免文本过长导致布局溢出
const text = widget.addText(longContent);
text.lineLimit = 2;
text.minimumScaleFactor = 0.8;

4. 设置合理的刷新时间

// 推荐 - 根据数据更新频率设置
widget.refreshAfterDate = Date.now() / 1000 + 1800; // 30 分钟

// 不推荐 - 过于频繁会消耗电量
widget.refreshAfterDate = Date.now() / 1000 + 60; // 1 分钟(iOS 可能会忽略)

5. 使用语义字体

// 推荐 - 自动适配用户的字体大小偏好
title.font = Font.headline();
body.font = Font.body();

// 也可以 - 需要精确控制大小时
title.font = Font.boldSystemFont(20);

注意事项

  1. Widget 尺寸: iOS 提供 Small、Medium、Large 三种尺寸,你的布局需要适配所有尺寸
  2. 刷新限制: iOS 限制 Widget 刷新频率,refreshAfterDate 只是建议值,系统可能延迟刷新
  3. 无交互: Widget 不支持按钮等交互,url 属性用于打开链接(整个 Widget 或单个元素)
  4. 图片限制: 建议使用 SF Symbol 或小尺寸本地图片,网络图片需要先下载到本地
  5. 深色模式: 使用 Color.dynamic(lightColor, darkColor) 适配深色模式
  6. 内存限制: Widget Extension 内存有限(约 30MB),避免加载大量图片
  7. 序列化: Widget 树通过 JSON 序列化传递,不支持函数/闭包属性
  8. Scriptable 兼容: API 与 Scriptable 保持一致,presentSmall() 等预览方法为兼容保留
  9. 脚本元数据: Script._widgetSlotScript._widgetScriptNameScript._widgetScriptId 由系统自动注入
  10. App Group: Widget 数据存储在 App Group 共享容器中,确保 App 和 Extension 使用相同的 Group ID