实时活动基础知识
Live Activities essentials
2026年6月15日
一句话判断
这场 Session 最值得关注的一件事是:灵动岛(Dynamic Island)终于支持横屏了,Apple 提供了一套全新的环境变量(Environment Values)让你用极少的代码搞定横屏、待机显示(StandBy)和 Apple Watch 的 UI 适配灾难。
这场 Session 讲了什么
以前做实时活动(Live Activities),大家基本只盯着竖屏锁屏和灵动岛,横屏打游戏或看视频时这玩意儿就直接装死。到了 iOS 27,Apple 把实时活动的展示面铺到了横屏灵动岛、StandBy、Apple Watch 智能叠放(Smart Stack)甚至 CarPlay。为了解决多端尺寸差异带来的 UI 崩溃,系统引入了 isDynamicIslandLimitedInWidth 和 activityFamily 等环境变量,让你能根据当前空间大小精准裁剪内容。
对做外卖、打车、赛事直播的 App 影响最大。你不再需要为不同设备写一堆独立的 Widget,而是通过一套 ActivityConfiguration 结合环境判断,就能让同一个实时活动在所有苹果设备上“看起来都很对”。另外,新增的 LiveActivityIntent 让实时活动从“只能看”变成了“能直接操作”,用户不用跳回 App 就能完成打分、确认等轻量交互。
值得深挖的点
横屏灵动岛与多端 UI 的“优雅降级”
以前横屏时灵动岛是不显示实时活动的,因为横屏状态下刘海或挖孔区域变窄,高度受限,塞不下原来的 UI。iOS 27 补齐了这个场景,但代价是宽度被严重压缩。Apple 没有简单粗暴地等比缩放,而是给了 isDynamicIslandLimitedInWidth 这个环境变量。
这个设计的 trade-off 很明显:把适配的锅甩给了开发者,但换来了像素级的完美展示。当检测到宽度受限时,你可以把原本显示“预计 15:30 送达”的文本,降级成一个简单的进度图标。同样,在 StandBy 模式下,通过 showsWidgetContainerBackground 判断是否需要自己画背景,配合 activityBackgroundTint 设置底色,能避免锁屏上好看的渐变背景在 StandBy 里变成一块突兀的小补丁。这种“一套代码,多处判断”的策略,比以前针对不同设备族写死尺寸要优雅得多。
LiveActivityIntent 打通了交互的最后一公里
之前的实时活动,用户点击后只能无脑跳转回 App 的某个页面,这在很多场景下非常反人类。比如咖啡做好了,我只想点个“好评”,却要我打开 App、等启动图、找订单、再点好评。
这次引入的 LiveActivityIntent 直接让实时活动具备了原生交互能力。它复用了 App Intents 框架,你只需要让 Intent 遵循 LiveActivityIntent 协议,就能在灵动岛展开视图或锁屏界面里直接放按钮。点击后,系统会在后台静默执行 perform() 方法,甚至可以更新本地数据库或发网络请求。这不仅减少了 App 的冷启动次数,还让实时活动真正变成了一个“微型 App”。坑点在于,Intent 的执行环境受限,不能做太重的 UI 弹窗操作,必须保持轻量。
代码片段
1. 搞定横屏灵动岛的宽度限制
场景:在灵动岛紧凑视图(Compact View)中,根据横屏宽度受限状态切换显示文本或图标。
struct CompactTrailingView: View {
// 获取当前灵动岛宽度是否受限的环境变量
@Environment(\.isDynamicIslandLimitedInWidth) var isWidthLimited
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
if isWidthLimited {
// 横屏宽度不够时,只显示一个进度图标
StepProgressIconView(context: context)
} else {
// 竖屏空间充足时,显示具体的预计时间
EstimatedReadyView(context: context)
}
}
}
坑:千万别在 isWidthLimited 为 true 时还硬塞长文本,系统不会帮你自动截断,只会导致 UI 溢出或布局错乱。
2. 完美适配 StandBy 和 Apple Watch
场景:让实时活动在待机显示(StandBy)中铺满背景,并在 Apple Watch 上展示精简布局。
struct ActivityView: View {
// 判断当前是否在 StandBy 等需要自定义背景的场景
@Environment(\.showsWidgetContainerBackground) var showsBackground
// 获取当前活动的设备族(如 .small 代表 Watch/CarPlay)
@Environment(\.activityFamily) var activityFamily
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
contentView
.background {
if showsBackground {
// StandBy 下使用渐变背景铺满
LinearGradient.barista
}
}
// 设置系统级别的背景底色
.activityBackgroundTint(.espresso)
}
@ViewBuilder
var contentView: some View {
if activityFamily == .small {
// Apple Watch 或 CarPlay 上的极简视图
WatchCompactView(context: context)
} else {
// iPhone 锁屏的完整视图
LockScreenFullView(context: context)
}
}
}
坑:activityFamily 为 .small 时,可用空间极小,务必砍掉所有非核心信息,只保留最关键的数字或状态图标。
3. 在实时活动中直接执行操作
场景:用户在不打开 App 的情况下,直接在锁屏或灵动岛给咖啡订单打分。
struct RateDrinkIntent: LiveActivityIntent {
static var title: LocalizedStringResource = "评价饮品"
@Parameter(title: "订单 ID")
var orderID: String
@Parameter(title: "是否好评")
var isPositive: Bool
func perform() async throws -> some IntentResult {
// 在后台执行打分逻辑,比如发送网络请求
await NetworkService.shared.submitRating(orderID: orderID, isPositive: isPositive)
// 更新本地数据并设置 15 秒后自动结束实时活动
await updateLocalDatastore(rating: isPositive ? .great : .poor, dismissPolicy: .after(.now + 15))
return .result()
}
}
坑:perform() 方法是在后台执行的,不要在这里尝试弹出 UIAlertController 或进行任何阻塞主线程的 UI 操作。
最佳实践
已有项目的迁移策略:如果你之前的实时活动只写了锁屏和灵动岛竖屏视图,现在必须加上 isDynamicIslandLimitedInWidth 和 activityFamily 的判断。不用重写整个 UI,只需要在现有的 SwiftUI 视图外层套一层条件判断,把长文本换成图标,把复杂列表换成单行状态即可。
新项目的采用建议:严格分离静态数据和动态数据。把永远不变的字段(如店铺名、订单号)放在 ActivityAttributes 里,把随时间变化的字段(如状态、倒计时)放在 ContentState 里。这不仅是 Apple 的强制要求,更能大幅减少推送更新的 payload 体积。
实战中容易踩的坑:推送更新策略的选择。如果你的 App 是赛事直播,几万人同时看一场比赛,一定要用广播频道(Channel)推送更新,而不是给每个设备单独发 Push。单播推送在并发量高时极易触发 APNs 的限流,导致部分用户的实时活动卡死在旧状态。
还有什么值得关注
staleDate的妙用:在ContentState中设置staleDate,当超过这个时间系统还没收到新推送时,UI 会自动变灰或显示过期样式,防止用户看到错误的倒计时。- 提前安排实时活动:除了前台直接启动,现在支持通过
ActivityKit提前 schedule 一个实时活动,比如在用户买完电影票后,安排电影开场前一小时自动弹出。 - CarPlay 自动转发:只要你的实时活动支持了
.small家族,系统会自动把它转发到 CarPlay 仪表盘,不需要额外写 CarPlay 专属代码。