Expo 中的主屏幕小组件与实时活动

新的 expo-widgets 库让你可以用 Expo UI 组件构建主屏幕小组件和实时活动,无需任何原生配置。

中文
复制
Home screen widgets and Live Activities in Expo

主屏幕小组件和实时活动是 iOS 应用能提供的最显眼的功能之一。做得好的小组件能让应用在关闭状态下依然留在用户的生活里,从而提升留存和活跃度。在竞争激烈的 App Store 里,这一点越来越重要。

直到不久之前,从 React Native 应用发布一个小组件还意味着一堆原生流程:单独的 Xcode target、用于共享数据的 App Groups、SwiftUI 布局代码,以及让这个扩展与应用的其余部分保持同步的长期负担。

这篇文章介绍 Expo Widgets 的 alpha 版本——这个大家期待已久的新库可以用 Expo UI 组件构建主屏幕小组件和实时活动,原生部分的配置交给 Continuous Native Generation 处理。

什么是小组件和实时活动?

小组件是位于主屏幕或锁定屏幕上的小型、可一瞥查看的 UI 界面。它们由系统按计划渲染,而不是由应用实时渲染。小组件可以深链到应用的特定页面,从 iOS 17 起还支持按钮和开关等交互。

实时活动是出现在锁定屏幕和灵动岛上的限时更新。事件开始时(配送、比赛、行程)由应用启动一个实时活动,随后随着状态变化更新,更新可以来自应用本身,也可以通过 APNs 推送通知。事件结束后,实时活动被关闭。

把小组件带到 React

Evan Bacon 的 expo-apple-targets 自动化了 Xcode 的配置,但你仍然要用 SwiftUI 编写原生组件。

expo-widgets 则允许你用 React 组件来定义小组件。config plugin 会生成 Widget Extension target、配置 App Group,并在 prebuild 期间创建所有必要的文件。

让这一切成为可能的关键是 @expo/ui。小组件扩展无法在渲染时运行 React Native——它们由系统按需渲染,时间预算很紧,而且可能完全在应用未运行的情况下进行。布局必须用原生方式表达。

@expo/ui 暴露了 TextVStackHStackImage 等 React 组件,它们与 SwiftUI 原生组件一一对应。当系统请求 widget 时间线时,你的组件会在独立的 JS 运行时中执行,生成一棵 @expo/ui 布局树。随后,原生端会根据这份描述,用 SwiftUI 视图重建 UI——实际渲染过程完全不涉及 React Native。

Widgets

Widget 就是普通的 React 组件,但有几条重要限制:

  • 必须自包含

  • 必须用 widget 指令标记

  • 展示数据通过 props 传入(外加几个 widget 专属字段)

数据会以 props 的形式传入,其中包含 'family' 等额外字段,用来标明当前填充的是哪种尺寸,这样你无需为每种尺寸单独定义 widget,就能让布局自适应。之后你可以提前把数据和对应的时间线排好,系统会自动显示正确的那一个。

部件

Live Activities

Live Activities 遵循同样的思路(用 Expo UI 描述布局,推送 props 来更新),但布局中有多个“槽位”,对应它可以出现的位置:

  • 锁屏横幅

  • Dynamic Island 紧凑模式(leading + trailing)

  • Dynamic Island 最小模式

  • Dynamic Island 展开模式(leading、center、trailing、bottom)

你还可以通过 APNs 直接从服务器推送更新,甚至借助 push-to-start token 在用户毫无操作的情况下远程启动 activity。

实时活动

如何创建第一个 Widget

安装好库并配置好第一个 widget 之后,一个最简 widget 长这样——它记录咖啡数量,并允许用户直接在主屏幕上加一:

import { Button, Text, VStack } from '@expo/ui/swift-ui';
import { font, foregroundStyle } from '@expo/ui/swift-ui/modifiers';
import { createWidget, WidgetBase } from 'expo-widgets';

type Props = { count: number };

const CoffeeCounter = (p: WidgetBase<Props>) => {
  'widget';

  return (
    <VStack spacing={8}>
      <Text modifiers={[font({ size: 48 })]}>☕</Text>
      <Text modifiers={[font({ size: 32, weight: 'bold' })]}>{p.count}</Text>
      <Button
        modifiers={[foregroundStyle('white')]}
        label="+"
        target="increment"
        onPress={() => ({ count: p.count + 1 })}
      />
    </VStack>
  );
};

export default createWidget('CoffeeCounter', CoffeeCounter);

函数体顶部的 'widget' 指令将它标记为 widget。onPress 返回一次局部状态更新——它会与当前状态合并并重新渲染。无需启动 App。

Widget 与 Live Activity 的适用场景

凡是用户一天中瞥一眼就能受益的内容,都适合做成 widget:即将到来的日程、当前天气、任务数量,或者习惯打卡的连续天数。

Live Activities 最适合有明确终点的持续事件:外卖配送的预计送达倒计时、带比赛计时的实时比分、航班状态更新、网约车司机位置,或运动计时器。关键在于,这类活动有自然的开始时机,也有自然的结束时机。

有一条好用的原则:小组件用于“查看状态”,Live Activities 用于“正在进行”。

接下来

我们计划加入几项内容:

  • 时间线回读——从 App 中取回 App 关闭期间已应用的所有时间线更新,以便把状态同步回你的 App 逻辑。

  • 刷新策略——在安排时间线时指定刷新策略,告诉 WidgetKit 何时请求新的时间线,例如在最后一条条目显示之后,或在某个具体日期。

  • 图片——ExpoUI 目前不支持显示图片。

expo-widgets 目前处于 alpha 阶段。随着我们收集反馈、处理粗糙之处,部分 API 可能会变化。

我们很期待看到你构建的东西,也想知道你的想法。如果有哪里不能用,或者你有功能需求,告诉我们

来源: Expo Blog← 返回首页