关于 shareplay-activities
shareplay-activities 是面向 Apple 应用开发者的 GroupActivities 与 SharePlay 技能,用于构建实时共享体验。它介绍会话生命周期、参与者间状态同步、协同媒体播放及附件传输。
主要功能
- 01定义并启动 GroupActivity
- 02管理 SharePlay 会话生命周期
- 03通过消息同步参与者状态
- 04使用 AVPlaybackCoordinator 协同播放
- 05通过 GroupSessionJournal 传输附件
适用场景
- 01构建 FaceTime 中的共同观看体验
- 02同步协作绘图或编辑状态
- 03向新加入者提供当前活动状态
- 04在会话中共享非实时附件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill shareplay-activities安装后在 Claude Code 中输入/shareplay-activities确认已加载。
附带文件evals/evals.jsonreferences/shareplay-patterns.md
详细介绍
GroupActivities / SharePlay
使用 GroupActivities 框架构建共享实时体验。SharePlay 可通过 FaceTime、Messages、AirDrop 和附近的 visionOS 共享功能连接用户,并同步媒体播放、应用状态或自定义数据。
目录
设置
功能
在 Xcode 中为应用目标添加 Group Activities 功能。Xcode 会添加所需的授权,并更新配置文件:
<key>com.apple.developer.group-session</key>
<true/>
仅为应用目标配置此功能。小组件、扩展和 App Clips 不支持 Group Activities。
检查是否符合条件
import GroupActivities
let observer = GroupStateObserver()
// Check if a FaceTime call or Messages conversation is active
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、通过 AirDrop 使用 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)
// Observe session state changes
let stateTask = Task {
for await state in session.$state.values {
handleState(state)
}
}
sessionTasks.append(stateTask)
// Observe participant changes
let participantTask = Task {
for await participants in session.$activeParticipants.values {
handleParticipants(participants)
}
}
sessionTasks.append(participantTask)
// Join the session
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("Waiting to join")
case .joined:
print("Joined session")
loadActivity(session?.activity)
case .invalidated(let reason):
print("Session ended: \(reason)")
cleanUp()
@unknown default:
break
}
}
private func handleParticipants(_ participants: Set<Participant>) {
print("Active participants: \(participants.count)")
}
离开和结束
// Leave the session (other participants continue)
session?.leave()
// End the session for all participants
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)
}
// Send to specific participants
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)
}
}
}
传递模式
// Reliable (default) -- checked and retried for crucial state
let reliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .reliable
)
// Unreliable -- lower latency, no delivery guarantee
let unreliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .unreliable
)
对于选择或轮次等会改变状态的操作,使用 .reliable。对于光标位置、绘图笔画和反应等高频临时数据,使用 .unreliable。
协调媒体播放
对于视频或音频,使用 AVPlayer 配合 AVPlaybackCoordinator:
import AVFoundation
import GroupActivities
func configurePlayback(
session: GroupSession<WatchTogetherActivity>,
player: AVPlayer
) {
// Connect the player's coordinator to the session
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:
// A conversation is active and the user chose to share.
_ = try await activity.activate()
case .activationDisabled:
// The user chose local playback, or sharing is unavailable.
startLocalExperience()
case .cancelled:
break
@unknown default:
break
}
}
没有活跃对话时(即 isEligibleForGroupSession 为 false),
使用 GroupActivitySharingController 让用户先选择联系人:
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)
自定义控件请使用 shareplay SF Symbol。将 GroupActivityMetadata
视为发现文案:标题、副标题、图片和类型应简洁,并与入口相符。避免涉及相邻领域:GameKit 负责身份验证、匹配、排行榜、成就和语音/聊天;TabletopKit 负责席位、棋盘设备、空间布局、回合、规则和权威桌面状态;AVKit 负责播放界面。SharePlay 负责邀请、生命周期、参与者和协调交接。SwiftUI ShareLink、AirDrop 和直接激活模式请参阅 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)
// Upload a Transferable file or data item
let attachment = try await journal.add(sharedImageItem)
// Observe incoming attachments
Task {
for await attachments in journal.attachments {
for attachment in attachments {
let data = try await attachment.load(Data.self)
handleReceivedFile(data)
}
}
}
常见错误
不要:忘记调用 session.join()
配置存储的会话、信使和观察器,然后调用 join()。会话生命周期中的规范长生命周期管理器展示了所需顺序。
不要:忘记离开或结束会话
// WRONG -- session stays alive after the user navigates away
func viewDidDisappear() {
// Nothing -- session leaks
}
// CORRECT -- leave when the view is dismissed
func viewDidDisappear() {
session?.leave()
session = nil
messenger = nil
}
不要:假设所有参与者的状态都相同
// WRONG -- broadcasting state without handling late joiners
func onJoin() {
// New participant has no idea what the current state is
}
// CORRECT -- send full state to new participants
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 传输用于大型或受保护的资源
// WRONG -- messenger is small/time-sensitive; journal is Transferable and <=100 MB
let imageData = try Data(contentsOf: imageURL) // 300 KB
try await messenger.send(imageData, to: .all) // Too large
// CORRECT -- journal attachments up to 100 MB; otherwise share a pointer/manifest
let journal = GroupSessionJournal(session: session)
try await journal.add(sharedImageItem)
// Larger/protected assets: server storage or app-managed file transfer
不要:为媒体播放发送冗余消息
// WRONG -- manually syncing play/pause when using AVPlayer
func play() {
player.play()
try await messenger.send(PlayMessage(), to: .all)
}
// CORRECT -- let AVPlaybackCoordinator handle it
player.playbackCoordinator.coordinateWithSession(session)
player.play() // Automatically synced to all participants
不要:在会被重新创建的视图中观察会话
应在长生命周期管理器中拥有 sessions() 监听器,而不是在可重新创建的视图中。使用上文展示的管理器生命周期,并在失效时取消其子任务。
审查清单
- 仅向应用目标添加 Group Activities 功能
-
GroupActivity结构体遵循Codable,且包含有意义的元数据 - 使用
ShareLink、AirDrop 或共享表单时添加Transferable一致性 - 在长生命周期对象(而非 SwiftUI 视图主体)中观察
sessions() - 接收并配置会话后调用
session.join() - 用户离开页面或关闭界面时调用
session.leave() -
GroupSessionMessenger消息保持在 256 KB 以内,并使用恰当的deliveryMode - 新加入的参与者在连接时收到当前状态
- 观察
$state和$activeParticipants发布者以处理生命周期变化 - 使用
GroupSessionJournal传输非紧急的Transferable附件 - 使用
AVPlaybackCoordinator同步媒体(而非手动发送消息) - 显示 SharePlay 界面前检查
GroupStateObserver.isEligibleForGroupSession - 没有活跃对话时使用
GroupActivitySharingController - 会话失效时清理信使、日志和任务
参考资料
- 扩展模式(SwiftUI 共享、协作画布、空间 Persona):references/shareplay-patterns.md
- 配置群组活动
- GroupActivities 框架
- GroupActivity 协议
- GroupSession
- GroupSessionMessenger
- GroupSessionJournal
- GroupStateObserver
- GroupActivitySharingController
- 定义应用的 SharePlay 活动
- 从应用的 UI 中发起 SharePlay 活动
- 在 SharePlay 活动期间同步数据
- 支持协调式媒体播放
- SharePlay HIG