iOS 小组件和实时活动在 Expo SDK 56 中已趋于稳定
SDK 56 将 expo-widgets 提升为稳定版。用 React 组件即可构建主屏幕小组件和锁屏实时活动,无需 SwiftUI。
中文
复制

Widget 和 Live Activity 最适合那些打开 App 反而多此一举的场景。外卖预计送达时间、运动计时、航班状态、下一场会议、一个保存好的快捷指令——人们想要这类信息时,手头往往正忙着别的事。Widget 和 Live Activity 让你的 App 保持有用,甚至不需要用户打开它。
过去,想在 React Native App 里做这些界面,意味着要单独建一个 Xcode target、配置 App Groups、写 SwiftUI 布局代码,还得长期维护这个扩展,让它和 App 其余部分保持同步。
从 SDK 56 开始,expo-widgets 已进入稳定版。你可以用 Expo UI 把 Widget 和 Live Activity 写成 React 组件。Widget Extension target、App Group 配置和 SwiftUI 脚手架都由 Continuous Native Generation 处理。不再需要维护一个单独的 iOS 工程。
这就是 SDK 55 中以 alpha 版本发布的那个库,经过打磨后转正。核心思路没变:用 React 写 Widget,交给系统通过 SwiftUI 渲染,更新则由你的 Expo App 管理。SDK 56 带来的是稳定性承诺和一批实用改进:Widget 和 Live Activity 现在直接接收自己的 environment,不再需要由主 App 预渲染,错误处理也更完善了。
为什么值得做 Widget 和 Live Activity
做 Widget 和 Live Activity 的理由不是 UI 的完整性,而是触达面。
App 图标需要用户点一下才能进入你的 App。Widget 是零次点击,并且能在用户忙别的事时提供价值。Live Activity 会在事件进行期间持续显示,随状态变化更新,锁屏时也不例外。当用户不需要完整打开 App 时,这些界面把相关信息留在近处,与 App 形成互补。
Flighty 是一个很好的公开案例。它拿下了 2023 年 Apple 设计奖的交互类别,Apple 的评语点出了这里最关键的部分:Live Activity、灵动岛集成、Widget 支持,以及把关键航班信息放在旅客最需要的地方。对一个航班追踪 App 来说,这就是产品体验本身。旅客想知道登机口变更、登机状态和到达时间,而不想反复打开 App。Flighty 把这些信息放到锁屏和灵动岛上,让它们在旅程真正进行时随手可得。
在生产环境中真正有效的模式:
-
用 Widgets 展示用户希望随手可及的信息(日历、天气、习惯打卡、余额),
-
用 Live Activities 展示有明确起止时间的“进行中”事件(外卖预计送达、锻炼计时、网约车、飞行中的航班)。
两者都能让 App 在用户没有主动打开它的时候依然保持存在感。
OneSignal 的 2024 年客户互动状况报告(数据有点旧,但足够具体)发现,使用 iOS Live Activities 的 App 平均 30 日留存率高出 23.7%。这并不是说加上一个 Live Activity 留存率就会自动提升,但它指向同一个产品规律:及时的信息,出现在合适的位置,能让用户更容易回到 App。
SDK 56 稳定了什么?
alpha 版随 3 月的 SDK 55 一起发布。此后,社区测试和内部迭代暴露出了那些必须打磨掉的粗糙之处,我们才敢说这个库已经稳定。主要变化:
Widgets 和 Live Activities 现在通过 WidgetEnvironment 获得完整的环境信息,包括 widget family、内容边距、配色方案、渲染模式,以及展示和无障碍相关的上下文,帮助 widget 适配系统展示它们的位置和方式。
Widgets 和 Live Activities 不再需要在 App 内预渲染。 过去要在启动时遍历 App 的 React 树来准备 widget 布局,现在 widget bundle 可以独立渲染,只需拿到当前展示面所需的上下文。
可配置的 widget。 SDK 56 加入了对可配置 widget 的支持,用户可以直接在 widget 图库中选择 widget 显示什么,而不必先在 App 里做完所有选择。
构建你的第一个 widget
下面是构建第一个 widget 的一些说明。如果你想看视频教程,可以点这里:
安装库并运行 prebuild 之后,一个最小 widget 长这样:
import { Button, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, type WidgetEnvironment } from 'expo-widgets';
type Props = { count: number };
const CoffeeCounter = (props: Props, environment: WidgetEnvironment) => {
'widget';
return (
<VStack spacing={8}>
<Text modifiers={[font({ size: 48 })]}>☕</Text>
<Text modifiers={[font({ size: 32, weight: 'bold' })]}>
{props.count}
</Text>
<Button
modifiers={[foregroundStyle('white')]}
label="+"
target="increment"
onPress={() => ({ count: props.count + 1 })}
/>
</VStack>
);
};
export default createWidget('CoffeeCounter', CoffeeCounter);
'widget' 指令把函数体标记为 widget 渲染上下文。它运行在与主应用隔离的沙箱 JS 运行时中。Expo UI 组件直接对应 SwiftUI 原语,因此系统会原生渲染 widget,而你的 widget 逻辑仍然留在 TypeScript 里。
Live Activity 遵循同样的模型,只是你需要描述多个槽位(锁屏横幅、Dynamic Island 的紧凑、展开和最小形态),系统会根据上下文挑选合适的一个。更新可以通过 APNs 从服务器推送,也可以由应用直接触发。
Widget 与 Live Activity:如何取舍
上面这段简述足以应付大多数产品讨论,但两者在实现上的取舍并不相同。先想清楚你希望向用户展示什么信息。
如果做 widget,要决定哪些信息值得在应用之外占据一个固定位置、它需要多久变化一次、哪些尺寸有用,以及用户点击它时应该发生什么。最好的 widget 通常只做一件明确的事。
如果做 Live Activity,先定义生命周期。什么会启动这个 activity?它在活跃期间可以发生哪些变化?结束时应该留下什么最终状态?更新来自应用、服务器,还是两者都有?这些问题很重要,因为 Live Activity 涉及推送 token、服务端更新逻辑、状态转换和结束处理。
如果你在两者之间犹豫,通常从 widget 入手更好。它的环节更少,是熟悉渲染模型的实用方式。等这套流程用顺手了,再为产品中那些有明确开始、活跃更新和结束的部分加上 Live Activity。
从 expo-widgets 开始
安装 expo-widgets,然后参照 widgets 文档 搭建你的第一个 widget。
挑一条用户在应用之外看到会受益的信息,先把那个 widget 做出来。
如果你的应用天然存在“进行中”的时刻,不妨为这个流程做一个实时活动原型。
如果你在开发中遇到问题,或者对下一步该做什么有想法,欢迎在 Discord 或 GitHub 上告诉我们。我们尤其关注生产环境中的部署案例,以及你愿意分享的留存或互动数据。真实的数字会影响我们接下来的优先级。