WWDC Quick Look 💓 By SwiftGGTeam
Meet ActivityKit

Meet ActivityKit

观看原视频

Highlight

ActivityKit 让开发者可以用 SwiftUI 和 WidgetKit 在锁屏、灵动岛和 StandBy 上创建实时活动,支持本地更新和远程推送更新,iOS 17 还新增了对 iPad 和交互式控件的支持。

核心内容

用户想随时知道外卖到哪了、比赛比分如何、打车还有多久到。以前这些信息要么需要打开 app 查看,要么靠频繁推送通知打扰用户。

Live Activity 解决这个问题的方式是在锁屏和灵动岛上持续显示关键信息。它有明确的开始和结束,实时更新进度,用户扫一眼就能获得所需信息。

在 iPhone 14 Pro 和 Pro Max 上,Live Activity 会出现在灵动岛。单个活动时显示 compact 形态,两个活动时都变成 minimal 形态。长按可以展开 expanded 形态查看更多信息。

iOS 17 带来了三个新能力:StandBy 模式下 Live Activity 会放大显示;iPad 也支持了 Live Activity;可以用 WidgetKit 添加按钮、切换等交互控件。

详细内容

定义 ActivityAttributes

05:40)每个 Live Activity 需要定义静态和动态数据:

import ActivityKit

struct AdventureAttributes: ActivityAttributes {
    let hero: EmojiRanger  // 静态数据:创建后不变

    struct ContentState: Codable & Hashable {
        let currentHealthLevel: Double  // 动态数据:会更新
        let eventDescription: String
    }
}

关键点:

  • ActivityAttributes 协议定义静态属性
  • 嵌套的 ContentState 定义动态状态,必须实现 CodableHashable
  • 静态数据在创建时确定,动态数据可以实时更新

启动 Live Activity

06:28)在 app 前台启动 Live Activity:

let adventure = AdventureAttributes(hero: hero)

let initialState = AdventureAttributes.ContentState(
    currentHealthLevel: hero.healthLevel,
    eventDescription: "Adventure has begun!"
)
let content = ActivityContent(
    state: initialState,
    staleDate: nil,
    relevanceScore: 0.0
)

let activity = try Activity.request(
    attributes: adventure,
    content: content,
    pushType: nil  // nil 表示仅本地更新
)

关键点:

  • 只能在 app 前台请求 Live Activity
  • 应该在用户明确操作后启动(如”开始追踪”)
  • staleDate 标记内容何时过期
  • relevanceScore 决定多个 Live Activity 的排序,越高越靠前
  • pushType: nil 表示只通过本地代码更新,设为 .token 可接收远程推送

更新 Live Activity

08:00)状态变化时调用 update

let heroName = activity.attributes.hero.name
let contentState = AdventureAttributes.ContentState(
    currentHealthLevel: hero.healthLevel,
    eventDescription: "\(heroName) has taken a critical hit!"
)

var alertConfig = AlertConfiguration(
    title: "\(heroName) has taken a critical hit!",
    body: "Open the app and use a potion to heal \(heroName)",
    sound: .default
)

activity.update(
    ActivityContent<AdventureAttributes.ContentState>(
        state: contentState,
        staleDate: nil
    ),
    alertConfiguration: alertConfig
)

关键点:

  • update 传入新的 ContentState 和可选的 AlertConfiguration
  • Alert 在 Apple Watch 上显示为通知,在 iPhone/iPad 上播放指定声音
  • 标题和 body 只在 Apple Watch 上使用

监听 Activity 状态

09:30)观察状态变化以便清理资源:

// 异步监听
func observeActivity(activity: Activity<AdventureAttributes>) {
    Task {
        for await activityState in activity.activityStateUpdates {
            if activityState == .dismissed {
                self.cleanUpDismissedActivity()
            }
        }
    }
}

// 同步获取
let activityState = activity.activityState
if activityState == .dismissed {
    self.cleanUpDismissedActivity()
}

四种状态:

  • .started — 已启动
  • .finished — 已结束
  • .dismissed — 用户已关闭
  • .stale — 内容已过期

结束 Live Activity

10:03)任务完成时结束活动:

let hero = activity.attributes.hero

let finalContent = AdventureAttributes.ContentState(
    currentHealthLevel: hero.healthLevel,
    eventDescription: "Adventure over! \(hero.name) has defeated the boss!"
)

let dismissalPolicy: ActivityUIDismissalPolicy = .default

activity.end(
    ActivityContent(state: finalContent, staleDate: nil),
    dismissalPolicy: dismissalPolicy
)

关键点:

  • .default 策略让结束后的内容在锁屏保留一段时间
  • 可以传入最终状态给用户一个总结

添加 Live Activity 到 WidgetBundle

10:50)Live Activity 配置需要加入 widget extension:

import WidgetKit
import SwiftUI

@main
struct EmojiRangersWidgetBundle: WidgetBundle {
    var body: some Widget {
        EmojiRangerWidget()
        LeaderboardWidget()
        AdventureActivityConfiguration()
    }
}

定义锁屏界面

11:05)锁屏和 StandBy 共用一套 UI:

struct AdventureActivityConfiguration: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: AdventureAttributes.self) { context in
            AdventureLiveActivityView(
                hero: context.attributes.hero,
                isStale: context.isStale,
                contentState: context.state
            )
            .activityBackgroundTint(Color.navyBlue)
        } dynamicIsland: { context in
            // 灵动岛配置...
        }
    }
}

关键点:

  • ActivityConfiguration 接收 attributes 类型和闭包
  • context 提供 attributesstateisStale
  • 不需要指定尺寸,系统自动决定
  • StandBy 会自动放大锁屏 UI 填满屏幕

定义灵动岛 Compact 形态

13:28)单个活动时的 compact 形态:

dynamicIsland: { context in
    DynamicIsland {
        // expanded 形态...
    } compactLeading: {
        Avatar(hero: context.attributes.hero)
    } compactTrailing: {
        ProgressView(value: context.state.currentHealthLevel) {
            Text("\(Int(context.state.currentHealthLevel * 100))")
        }
        .progressViewStyle(.circular)
        .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green)
    } minimal: {
        // minimal 形态...
    }
}

关键点:

  • compactLeadingcompactTrailing 分别位于灵动岛两侧
  • 空间有限,只放最关键的信息
  • 用户应该能通过这些内容识别出是哪个活动

定义灵动岛 Minimal 形态

14:42)两个活动同时存在时的 minimal 形态:

minimal: {
    ProgressView(value: context.state.currentHealthLevel) {
        Avatar(hero: context.attributes.hero)
    }
    .progressViewStyle(.circular)
    .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green)
}

关键点:

  • minimal 形态空间极小,只能放最关键的信息
  • 一个活动附着在 TrueDepth 相机旁,另一个 detached

定义灵动岛 Expanded 形态

15:26)长按展开后的 expanded 形态:

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        LiveActivityAvatarView(hero: hero)
    }
    DynamicIslandExpandedRegion(.trailing) {
        StatsView(hero: hero, isStale: isStale)
    }
    DynamicIslandExpandedRegion(.bottom) {
        HealthBar(currentHealthLevel: contentState.currentHealthLevel)
        EventDescriptionView(hero: hero, contentState: contentState)
    }
} compactLeading: {
    // ...
} compactTrailing: {
    // ...
} minimal: {
    // ...
}

关键点:

  • expanded 分为 .leading.trailing.bottom 三个区域
  • .center 区域留给系统显示时间
  • 视图可以添加 deep link,点击跳转到 app 对应页面

核心启发

1. 外卖/配送进度追踪

  • 做什么:用户下单后在锁屏和灵动岛实时显示骑手位置和预计送达时间
  • 为什么值得做:用户不需要反复打开 app 查看进度,体验提升明显
  • 怎么开始:定义 DeliveryAttributes 包含订单号(静态)和骑手位置、ETA(动态),用 ActivityConfiguration 设计简洁的进度条 UI

2. 运动/健身实时数据

  • 做什么:跑步、骑行时在灵动岛显示配速、心率和距离
  • 为什么值得做:运动时看手机不方便,灵动岛一瞥就能获得关键数据
  • 怎么开始:静态数据放运动类型和目标,动态数据放实时配速和距离,compact 形态显示当前配速,expanded 显示完整数据面板

3. 比赛/赛事比分直播

  • 做什么:关注一场球赛,在锁屏持续显示比分和关键事件
  • 为什么值得做:比推送通知更持续、更不打扰,用户随时扫一眼就知道比分
  • 怎么开始:结合远程推送更新(参考 10185 session),服务器推送进球、红黄牌等事件,本地更新 UI

4. 打车/网约车等待

  • 做什么:叫车后显示车辆距离、车牌号和预计到达时间
  • 为什么值得做:解决用户反复打开 app 看车到哪了的痛点
  • 怎么开始:静态数据放目的地和车型,动态数据放车辆位置和 ETA,minimal 形态只显示预计分钟数

关联 Session

评论

GitHub Issues · utterances