使用 GroupActivities 和 SharePlay 构建共享的实时体验。适用于实现共享媒体播放、协作应用功能、同步游戏状态,或任何在 iOS、macOS、tvOS 或 visionOS 上通过 FaceTime、信息、隔空投送或附近 visionOS 共享进行的群组活动。
GroupActivities / SharePlay
使用 GroupActivities 框架构建共享的实时体验。SharePlay 通过 FaceTime、信息、隔空投送和附近 visionOS 共享连接用户,同步媒体播放、应用状态或自定义数据。
目录
设置
功能
在 Xcode 中为应用目标添加 Group Activities 功能。Xcode 会添加所需的授权并更新配置文件:
<key>com.apple.developer.group-session</key>
<true/>
仅对应用目标进行配置。Group Activities 在小组件、扩展或 App Clips 中不可用。
检查资格
import GroupActivities
let observer = GroupStateObserver()
// 检查是否有活跃的 FaceTime 通话或信息对话
if observer.isEligibleForGroupSession {
showSharePlayButton()
}
响应式观察变化:
for await isEligible in observer.$isEligibleForGroupSession.values {
showSharePlayButton(isEligible)
}
定义 GroupActivity
遵循 GroupActivity 协议并提供元数据:
import GroupActivities
struct WatchTogetherActivity: GroupActivity {
let movieID: String
let movieTitle: String
var metadata: GroupActivityMetadata {
var meta = GroupActivityMetadata()
meta.title = movieTitle
meta.type = .watchTogether
meta.fallbackURL = URL(string: "https://example.com/movie/\(movieID)")
return meta
}
}
活动类型
| 类型 | 用例 |
|---|---|
.generic |
自定义活动的默认类型 |
.watchTogether |
视频播放 |
.listenTogether |
音频播放 |
.createTogether |
协作创作(绘图、编辑) |
.exploreTogether |
共享浏览、规划或探索 |
.learnTogether |
共享学习或研究 |
.readTogether |
共享阅读 |
.shopTogether |
共享购物 |
.workoutTogether |
共享健身会话 |
GroupActivity 遵循 Codable;存储的活动数据必须是可编码的。仅在使用 SwiftUI ShareLink、通过隔空投送进行 SharePlay 或 AppKit/UIKit 共享表单时添加 Transferable。保持负载最小:使用标识符或 URL 而不是大数据。
会话生命周期
监听会话
设置一个长期运行的任务,以便在其他参与者启动活动时接收会话:
@Observable
@MainActor
final class SharePlayManager {
private var session: GroupSession<WatchTogetherActivity>?
private var messenger: GroupSessionMessenger?
private var sessionTasks: [Task<Void, Never>] = []
func observeSessions() {
Task {
for await session in WatchTogetherActivity.sessions() {
self.configureSession(session)
}
}
}
private func configureSession(
_ session: GroupSession<WatchTogetherActivity>
) {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
// 观察会话状态变化
let stateTask = Task {
for await state in session.$state.values {
handleState(state)
}
}
sessionTasks.append(stateTask)
// 观察参与者变化
let participantTask = Task {
for await participants in session.$activeParticipants.values {
handleParticipants(participants)
}
}
sessionTasks.append(participantTask)
// 加入会话
session.join()
}
private func cleanUp() {
sessionTasks.forEach { $0.cancel() }
sessionTasks.removeAll()
session = nil
messenger = nil
}
}
会话状态
| 状态 | 描述 |
|---|---|
.waiting |
会话存在但本地参与者尚未加入 |
.joined |
本地参与者已活跃在会话中 |
.invalidated(reason:) |
会话结束(查看原因获取详情) |
处理状态变化
private func handleState(_ state: GroupSession<WatchTogetherActivity>.State) {
switch state {
case .waiting:
print("等待加入")
case .joined:
print("已加入会话")
loadActivity(session?.activity)
case .invalidated(let reason):
print("会话结束:\(reason)")
cleanUp()
@unknown default:
break
}
}
private func handleParticipants(_ participants: Set<Participant>) {
print("活跃参与者:\(participants.count)")
}
离开和结束
// 离开会话(其他参与者继续)
session?.leave()
// 结束所有参与者的会话
session?.end()
发送和接收消息
使用 GroupSessionMessenger 在参与者之间同步小型、时间敏感的应用状态。
定义消息
消息必须遵循 Codable;每条消息保持在 256 KB 以下。
struct SyncMessage: Codable {
let action: String
let timestamp: Date
let data: [String: String]
}
发送
func sendSync(_ message: SyncMessage) async throws {
guard let messenger else { return }
try await messenger.send(message, to: .all)
}
// 发送给特定参与者
try await messenger.send(message, to: .only(participant))
接收
func observeMessages() {
guard let messenger else { return }
Task {
for await (message, context) in messenger.messages(of: SyncMessage.self) {
let sender = context.source
handleReceivedMessage(message, from: sender)
}
}
}
传递模式
// 可靠(默认)——对关键状态进行校验和重试
let reliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .reliable
)
// 不可靠——低延迟,不保证送达
let unreliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .unreliable
)
对于改变状态的操作(如选择、回合),使用 .reliable。对于高频、短暂的数据(如光标位置、绘图笔触、反应),使用 .unreliable。
协调媒体播放
对于视频/音频,使用 AVPlaybackCoordinator 与 AVPlayer:
import AVFoundation
import GroupActivities
func configurePlayback(
session: GroupSession<WatchTogetherActivity>,
player: AVPlayer
) {
// 将播放器的协调器连接到会话
let coordinator = player.playbackCoordinator
coordinator.coordinateWithSession(session)
}
连接后,AVFoundation 会同步播放/暂停、跳转、速率、播放速度和时间。不要将 AVPlayer 传输字段放入 messenger 消息或快照(包括后加入者快照)中;仅对播放之外的状态使用自定义消息。
从应用启动 SharePlay
使用 GroupActivitySharingController (UIKit)
import GroupActivities
import UIKit
func startSharePlay() async throws {
let activity = WatchTogetherActivity(
movieID: "123",
movieTitle: "Great Movie"
)
switch await activity.prepareForActivation() {
case .activationPreferred:
// 有活跃对话且用户选择共享。
_ = try await activity.activate()
case .activationDisabled:
// 用户选择本地播放,或共享不可用。
startLocalExperience()
case .cancelled:
break
@unknown default:
break
}
}
当没有活跃对话时(即 isEligibleForGroupSession 为 false),使用 GroupActivitySharingController 让用户先选择联系人:
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)
为自定义控件使用 shareplay SF 符号。将 GroupActivityMetadata 视为发现文案:简洁的标题、副标题、图像和类型,与入口点一致。保持领域分离:GameKit 拥有认证、匹配、排行榜、成就和语音/聊天;TabletopKit 拥有座位、棋盘设备、空间放置、回合、规则和权威桌面状态;AVKit 拥有播放 UI。SharePlay 拥有邀请、生命周期、参与者和协调交接。有关 SwiftUI ShareLink、隔空投送和直接激活模式,请参阅 references/shareplay-patterns.md。
GroupSessionJournal:文件传输
对于较大、非时间敏感的附件,使用 GroupSessionJournal 而不是 GroupSessionMessenger。日志项必须遵循 Transferable,对后加入者可用,且限制为 100 MB。需要 iOS/iPadOS/tvOS 17+、macOS 14+ 或 visionOS 1+。对于更大/受保护的资源,共享指针或清单,并使用服务器存储或应用管理的文件传输。
import GroupActivities
let journal = GroupSessionJournal(session: session)
// 上传一个 Transferable 文件或数据项
let attachment = try await journal.add(sharedImageItem)
// 观察传入的附件
Task {
for await attachments in journal.attachments {
for attachment in attachments {
let data = try await attachment.load(Data.self)
handleReceivedFile(data)
}
}
}
常见错误
不要:忘记调用 session.join()
配置好存储的会话、messenger 和观察者后,调用 join()。会话生命周期中的规范长期管理器显示了所需的顺序。
不要:忘记离开或结束会话
// 错误——用户导航离开后会话仍然存活
func viewDidDisappear() {
// 什么都不做——会话泄漏
}
// 正确——视图消失时离开
func viewDidDisappear() {
session?.leave()
session = nil
messenger = nil
}
不要:假设所有参与者状态相同
// 错误——广播状态但不处理后加入者
func onJoin() {
// 新参与者不知道当前状态
}
// 正确——向新参与者发送完整状态
func handleParticipants(_ participants: Set<Participant>) {
let newParticipants = participants.subtracting(knownParticipants)
for participant in newParticipants {
Task {
try await messenger?.send(currentState, to: .only(participant))
}
}
knownParticipants = participants
}
不要:使用 SharePlay 传输大型/受保护资源
// 错误——messenger 用于小型/时间敏感数据;journal 是 Transferable 且 <=100 MB
let imageData = try Data(contentsOf: imageURL) // 300 KB
try await messenger.send(imageData, to: .all) // 太大
// 正确——journal 附件最大 100 MB;否则共享指针/清单
let journal = GroupSessionJournal(session: session)
try await journal.add(sharedImageItem)
// 更大/受保护资源:服务器存储或应用管理的文件传输
不要:为媒体播放发送冗余消息
// 错误——使用 AVPlayer 时手动同步播放/暂停
func play() {
player.play()
try await messenger.send(PlayMessage(), to: .all)
}
// 正确——让 AVPlaybackCoordinator 处理
player.playbackCoordinator.coordinateWithSession(session)
player.play() // 自动同步给所有参与者
不要:在会重建的视图中观察会话
将 sessions() 监听器放在长期运行的管理器中,而不是可重建的视图中。使用上面显示的管理器生命周期,并在失效时取消其子任务。
审查清单
- [ ] 仅对应用目标添加了 Group Activities 功能
- [ ]
GroupActivity结构体遵循Codable并包含有意义的元数据 - [ ] 使用
ShareLink、隔空投送或共享表单时添加了Transferable遵循 - [ ]
sessions()在长期对象中观察(而非 SwiftUI 视图体) - [ ] 接收并配置会话后调用了
session.join() - [ ] 用户导航离开或关闭时调用了
session.leave() - [ ]
GroupSessionMessenger消息保持在 256 KB 以下,并使用适当的deliveryMode - [ ] 后加入的参与者在连接时接收当前状态
- [ ] 观察
$state和$activeParticipants发布者以处理生命周期变化 - [ ] 对于非时间敏感的
Transferable附件使用GroupSessionJournal - [ ] 使用
AVPlaybackCoordinator进行媒体同步(而非手动消息) - [ ] 在显示 SharePlay UI 前检查
GroupStateObserver.isEligibleForGroupSession - [ ] 当没有活跃对话时使用
GroupActivitySharingController - [ ] 会话失效时处理清理,包括 messenger、journal 和任务
参考资料
- 扩展模式(SwiftUI 共享、协作画布、空间 Persona):references/shareplay-patterns.md
- 配置 Group Activities
- GroupActivities 框架
- GroupActivity 协议
- GroupSession
- GroupSessionMessenger
- GroupSessionJournal
- GroupStateObserver
- GroupActivitySharingController
- 定义应用的 SharePlay 活动
- 从应用 UI 呈现 SharePlay 活动
- 在 SharePlay 活动期间同步数据
- 支持协调媒体播放
- SharePlay HIG






