Highlight
iOS 27 让 Live Activities 从竖屏锁屏扩展到横屏灵动岛、StandBy、Apple Watch 和 CarPlay,开发者通过 ActivityKit 的数据模型分离、WidgetKit 的多尺寸视图配置以及 LiveActivityIntent 的交互能力,可以用一套代码在所有设备上提供实时信息更新。
核心内容
想象你正在用外卖 App 点了一杯咖啡。下单后你想知道什么时候能做好,但不想反复打开 App 查看。这就是 Live Activities 要解决的问题。
在 iOS 27 之前,Live Activities 只出现在竖屏锁屏和灵动岛。横屏打游戏、看视频时,这些信息就消失了。StandBy 模式下充电的 iPhone 也看不到进度。Apple Watch 和 CarPlay 更是完全无缘。
iOS 27 改变了这一切。横屏状态下灵动岛依然显示 Live Activities,只是宽度被压缩。StandBy 模式会把锁屏视图放大到 200% 展示。同一个 Live Activity 还会自动同步到 Apple Watch 的 Smart Stack、macOS 菜单栏和 CarPlay 仪表盘。
这意味着开发者不能只写一套 UI 就完事。横屏时宽度受限,StandBy 时背景需要铺满,Apple Watch 和 CarPlay 的空间极小。Apple 没有让开发者硬编码尺寸,而是提供了一组环境变量来判断当前展示场景,让同一套视图根据环境自动调整内容。
另一个变化是交互。以前用户点击 Live Activity 只能跳回 App。现在可以通过 LiveActivityIntent 直接在锁屏或灵动岛里完成轻量操作,比如给咖啡订单打分,全程不需要打开 App。
详细内容
数据模型:静态与动态分离
(04:16)
Live Activities 要求把数据分成两类:永远不会变的放在 ActivityAttributes 里,会随时间变化的放在 ContentState 里。这种分离让系统可以高效地推送更新,只传变化的部分。
以咖啡订单为例:
import ActivityKit
import Foundation
public struct DrinkOrderAttributes: ActivityAttributes {
let shopName: String
let drink: Drink
let orderID: UUID
public struct ContentState: Codable, Hashable {
var phase: DrinkOrder.Phase = .waiting
var estimatedReadyDate: Date
var rating: DrinkOrder.Rating?
}
}
关键点:
shopName、drink、orderID是静态数据,下单后不会变phase、estimatedReadyDate、rating是动态数据,会随订单进度更新ContentState必须遵循Codable和Hashable,因为系统要序列化后跨进程传递
多尺寸视图配置
(05:35)
Live Activity 的界面用 WidgetKit 构建。在 Widget Extension 里提供一个 ActivityConfiguration,定义各个展示位置的视图:
import ActivityKit
import SwiftUI
import WidgetKit
struct DrinkOrderLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DrinkOrderAttributes.self) { context in
ActivityView(context: context)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
ExpandedLeadingView(context: context)
}
DynamicIslandExpandedRegion(.center) {
ExpandedCenterView(context: context)
}
DynamicIslandExpandedRegion(.trailing) {
ExpandedTrailingView(context: context)
}
DynamicIslandExpandedRegion(.bottom) {
ExpandedBottomView(context: context)
}
} compactLeading: {
CompactLeadingView(context: context)
} compactTrailing: {
CompactTrailingView(context: context)
} minimal: {
MinimalView(context: context)
}
}
}
}
关键点:
ActivityConfiguration通过泛型参数DrinkOrderAttributes.self绑定数据模型content闭包返回锁屏和 StandBy 上显示的视图dynamicIsland闭包定义灵动岛的三种尺寸:expanded(长按展开)、compact(两侧小条)、minimal(多个活动并存时的最小圆点)- 每个视图通过
context参数读取当前的状态数据
启动与更新
(07:43)
启动 Live Activity 前,先检查用户是否授权:
func launchLiveActivity(order: DrinkOrder) throws {
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
let attributes = DrinkOrderAttributes(
shopName: "Coffee Shop",
drink: order.drink,
orderID: order.id
)
let estimatedReadyDate = Date.now + (15 * 60)
let contentState = DrinkOrderAttributes.ContentState(
phase: .waiting,
estimatedReadyDate: estimatedReadyDate
)
let activityContent = ActivityContent(state: contentState, staleDate: nil)
let activity = try Activity.request(attributes: attributes, content: activityContent)
}
更新时调用 update 方法:
await activity.update(
ActivityContent(
state: DrinkOrderAttributes.ContentState(
phase: .preparing,
estimatedReadyDate: estimatedReadyDate
),
staleDate: nil
)
)
关键点:
areActivitiesEnabled检查用户是否在设置中关闭了 Live ActivitiesstaleDate用于标记内容何时过期,过期后系统会显示过期状态提示用户- 更新可以通过前台代码直接调用,也可以通过推送通知在后台完成
- 推送更新有两种策略:广播频道(适合大规模并发场景,如赛事直播)和单设备推送(适合个性化场景)
横屏灵动岛的宽度适配
(10:33)
iOS 27 灵动岛在横屏时宽度受限,需要检测环境并切换内容:
struct CompactTrailingView: View {
@Environment(\.isDynamicIslandLimitedInWidth) var isDynamicIslandLimitedInWidth
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
if isDynamicIslandLimitedInWidth {
StepProgressIconView(context: context)
} else if context.state.phase.showsTimer {
EstimatedReadyView(context: context, font: .system(.body).monospacedDigit())
.multilineTextAlignment(.trailing)
.frame(maxWidth: maximumTimerLabelWidth)
} else {
OrderPhaseLabelView(context: context, font: .caption2.bold(), color: .brown)
.multilineTextAlignment(.trailing)
}
}
}
关键点:
isDynamicIslandLimitedInWidth只在横屏时返回true- 宽度受限时应该把文本替换为图标,避免布局溢出
- 竖屏时空间充足,可以显示倒计时或状态标签
StandBy 背景适配
(11:34)
StandBy 模式下锁屏视图被放大到 200%,原来的渐变背景会让内容显得很小。通过环境变量判断是否需要自己绘制背景:
struct ActivityView: View {
@Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
DetailView(context: context)
.background {
if showsWidgetContainerBackground {
LinearGradient.barista
}
}
.activityBackgroundTint(.espresso)
}
}
关键点:
showsWidgetContainerBackground在锁屏时为true,在 StandBy 时为false- 锁屏上保留渐变背景,StandBy 下用
.activityBackgroundTint设置全屏底色 - 这样 StandBy 模式下 Live Activity 会铺满整个屏幕区域
Apple Watch 和 CarPlay 的 small 适配
(12:30)
Apple Watch 和 CarPlay 的空间极小,需要声明支持 .small 活动家族并提供精简视图:
struct DrinkOrderLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DrinkOrderAttributes.self) { context in
ActivityView(context: context)
} dynamicIsland: { context in
// ... Dynamic Island 配置
}
.supplementalActivityFamilies([.small])
}
}
在视图内部根据 activityFamily 切换布局:
struct ActivityView: View {
@Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
@Environment(\.activityFamily) var activityFamily
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
contentView
.background {
if showsWidgetContainerBackground {
LinearGradient.barista
}
}
.activityBackgroundTint(.espresso)
}
@ViewBuilder
var contentView: some View {
if activityFamily == .small {
SmallView(context: context)
} else {
DetailView(context: context)
}
}
}
关键点:
.supplementalActivityFamilies([.small])声明支持小尺寸设备activityFamily为.small时代表 Apple Watch 或 CarPlay- 小尺寸视图必须极度精简,只保留最核心的一个数字或状态图标
- iPhone 上的 Live Activity 会自动转发到 Apple Watch 和 CarPlay,不需要额外代码
通过 App Intents 添加交互
(13:36)
让 Live Activity 从”只能看”变成”能操作”,需要定义一个遵循 LiveActivityIntent 的 Intent:
struct RateDrinkIntent: LiveActivityIntent {
static var title: LocalizedStringResource = "Rate Drink"
@Parameter(title: "Order ID")
var orderID: String
@Parameter(title: "Positive")
var isPositive: Bool
func perform() async throws -> some IntentResult {
await updateLocalDatastore(rating: isPositive ? .great : .poor, dismissPolicy: .after(.now + 15))
return .result()
}
}
在视图中把按钮和 Intent 关联:
struct RatingButtons: View {
var context: ActivityViewContext<DrinkOrderAttributes>
var body: some View {
HStack(spacing: 12) {
Button(intent: RateDrinkIntent(
orderID: context.attributes.orderID.uuidString, isPositive: false)) {
Label("Not Good", systemImage: "hand.thumbsdown.fill")
}
.buttonStyle(RatingButtonStyle(color: .red))
Button(intent: RateDrinkIntent(
orderID: context.attributes.orderID.uuidString, isPositive: true)) {
Label("Great", systemImage: "hand.thumbsup.fill")
}
.buttonStyle(RatingButtonStyle(color: .green))
}
}
}
关键点:
LiveActivityIntent继承自 App Intents 框架,执行环境在后台perform()方法里可以更新本地数据库、发送网络请求,但不能做 UI 弹窗dismissPolicy: .after(.now + 15)让 Live Activity 在操作完成后 15 秒自动消失- 按钮通过
Button(intent:)初始化器直接绑定 Intent,点击后系统静默执行
核心启发
做一个外卖进度追踪器
做什么:用户下单后,Live Activity 实时显示骑手位置、预计送达时间。横屏打游戏时也能在灵动岛看到进度。
为什么值得做:用户不需要反复打开 App 查看订单状态,实时信息常驻在锁屏和灵动岛。横屏状态下宽度受限时把地图缩略图换成配送中图标,确保信息始终可见。
怎么开始:定义 ActivityAttributes 和 ContentState 数据模型,创建 ActivityConfiguration,用 isDynamicIslandLimitedInWidth 在横屏时切换为图标展示。
做一个赛事比分速览
做什么:体育 App 在球赛进行时推送比分变化,Apple Watch 上通过 .small 家族只显示比分和剩余时间。
为什么值得做:几万人同时看同一场比赛,用广播频道推送更新避免 APNs 限流。多设备同步让用户在任何场景下都能快速获取比分,不需要打开 App。
怎么开始:用 Activity.request 启动 Live Activity,配置广播频道推送,声明 .supplementalActivityFamilies([.small]) 支持 Apple Watch 和 CarPlay。
做一个番茄钟专注助手
做什么:用户开始专注后,Live Activity 显示剩余时间。StandBy 模式下全屏显示倒计时。专注结束时让用户直接在锁屏上标记”完成”或”延长 5 分钟”。
为什么值得做:专注计时需要持续可见但不应打断当前工作。Live Activity 在锁屏和 StandBy 上常驻显示,通过 LiveActivityIntent 用户无需解锁就能操作。
怎么开始:创建遵循 LiveActivityIntent 的 Intent,在 perform() 中更新计时器状态,用 Button(intent:) 绑定到锁屏界面按钮。
做一个航班信息卡片
做什么:登机前 Live Activity 显示登机口、登机时间。起飞后自动更新为行李转盘信息。
为什么值得做:航班信息变化频繁,用户需要在关键时刻快速查看。用 staleDate 标记航班计划时间,如果延误超过该时间,UI 自动显示过期提示,避免用户看到过时的登机口信息。
怎么开始:启动时传入 ActivityContent(state:staleDate:),航班状态变化时调用 update 推送新状态,设置合适的 staleDate 标记信息有效期。
做一个智能家居快捷面板
做什么:空调、净化器等设备的状态通过 Live Activity 常驻显示。用户可以在锁屏上直接调节温度或开关设备。
为什么值得做:频繁查看和操作智能家居设备时,每次打开 App 都很繁琐。Live Activity 让状态常驻,结合 LiveActivityIntent 实现锁屏直接操作。
怎么开始:定义 LiveActivityIntent,在 perform() 中调用 HomeKit API 或自有后端,用 Button(intent:) 在 Live Activity 视图中绑定操作按钮。
关联 Session
- Design dynamic Live Activities — 学习如何设计在不同尺寸下都能清晰传达信息的 Live Activity 界面
- Bring your Live Activity to Apple Watch — 深入了解 Apple Watch Smart Stack 的适配细节
- SwiftUI essentials — 掌握 Live Activity 视图开发所需的 SwiftUI 基础
- App Intents advanced — 学习更复杂的 App Intents 交互模式,扩展 Live Activity 的操作能力
评论
GitHub Issues · utterances