shareplay-activities by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill shareplay-activities使用 GroupActivities 框架构建共享实时体验。SharePlay 通过 FaceTime 或 iMessage 连接用户,同步媒体播放、应用状态或自定义数据。目标平台为 Swift 6.2 / iOS 26+。
为您的应用添加 Group Activities 权限:
<key>com.apple.developer.group-session</key>
<true/>
对于无需 FaceTime 通话即可启动 SharePlay 的应用(iOS 17+),添加:
<key>NSSupportsGroupActivities</key>
<true/>
import GroupActivities
let observer = GroupStateObserver()
// 检查 FaceTime 通话或 iMessage 群组是否处于活动状态
if observer.isEligibleForGroupSession {
showSharePlayButton()
}
响应式观察变化:
for await isEligible in observer.$isEligibleForGroupSession.values {
showSharePlayButton(isEligible)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
遵循 GroupActivity 协议并提供元数据:
import GroupActivities
import CoreTransferable
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 | 协作创建(绘图、编辑) |
.workoutTogether | 共享健身会话 |
活动结构体必须遵循 Codable 协议,以便系统可以在设备之间传输它。
设置一个长期运行的任务,以便在其他参与者启动活动时接收会话:
@Observable
@MainActor
final class SharePlayManager {
private var session: GroupSession<WatchTogetherActivity>?
private var messenger: GroupSessionMessenger?
private var tasks = TaskGroup()
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)
// 观察会话状态变化
Task {
for await state in session.$state.values {
handleState(state)
}
}
// 观察参与者变化
Task {
for await participants in session.$activeParticipants.values {
handleParticipants(participants)
}
}
// 加入会话
session.join()
}
}
| 状态 | 描述 |
|---|---|
.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 的:
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)
}
一旦连接,任何参与者播放器上的播放/暂停/跳转操作都会自动同步到所有其他参与者。播放控制无需手动传递消息。
// 通知参与者播放事件
let event = GroupSessionEvent(
originator: session.localParticipant,
action: .play,
url: nil
)
session.showNotice(event)
import GroupActivities
import UIKit
func startSharePlay() async throws {
let activity = WatchTogetherActivity(
movieID: "123",
movieTitle: "Great Movie"
)
switch await activity.prepareForActivation() {
case .activationPreferred:
// 呈现分享控制器
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)
case .activationDisabled:
// SharePlay 被禁用或不可用
print("SharePlay 不可用")
case .cancelled:
break
@unknown default:
break
}
}
关于 ShareLink (SwiftUI) 和直接 activity.activate() 模式,请参阅 references/shareplay-patterns.md。
对于大型数据(图像、文件),使用 GroupSessionJournal 而不是 GroupSessionMessenger(后者有大小限制):
import GroupActivities
let journal = GroupSessionJournal(session: session)
// 上传文件
let attachment = try await journal.add(imageData)
// 观察传入的附件
Task {
for await attachments in journal.attachments {
for attachment in attachments {
let data = try await attachment.load(Data.self)
handleReceivedFile(data)
}
}
}
// 错误 —— 接收到会话但从未加入
for await session in MyActivity.sessions() {
self.session = session
// 会话将永远保持在 .waiting 状态
}
// 正确 —— 配置后加入
for await session in MyActivity.sessions() {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
session.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
}
// 错误 —— messenger 有每条消息的大小限制
let largeImage = try Data(contentsOf: imageURL) // 5 MB
try await messenger.send(largeImage, to: .all) // 可能失败
// 正确 —— 对文件使用 GroupSessionJournal
let journal = GroupSessionJournal(session: session)
try await journal.add(largeImage)
// 错误 —— 在使用 AVPlayer 时手动同步播放/暂停
func play() {
player.play()
try await messenger.send(PlayMessage(), to: .all)
}
// 正确 —— 让 AVPlaybackCoordinator 处理
player.playbackCoordinator.coordinateWithSession(session)
player.play() // 自动同步给所有参与者
// 错误 —— 每次视图出现时,都会创建一个新的监听器
struct MyView: View {
var body: some View {
Text("Hello")
.task {
for await session in MyActivity.sessions() { }
}
}
}
// 正确 —— 在长期存在的管理器中观察会话
@Observable
final class ActivityManager {
init() {
Task {
for await session in MyActivity.sessions() {
configureSession(session)
}
}
}
}
com.apple.developer.group-session)GroupActivity 结构体是 Codable 的,并具有有意义的元数据sessions()session.join()session.leave()deliveryMode 创建 GroupSessionMessenger$state 和 $activeParticipants 发布者以处理生命周期变化GroupSessionJournal 进行大型文件传输,而不是 messengerAVPlaybackCoordinator 进行媒体同步(而非手动消息)GroupStateObserver.isEligibleForGroupSessionprepareForActivation()references/shareplay-patterns.md每周安装量
325
代码仓库
GitHub 星标
269
首次出现
2026年3月8日
安全审计
安装于
codex322
opencode319
github-copilot319
amp319
cline319
kimi-cli319
Build shared real-time experiences using the GroupActivities framework. SharePlay connects people over FaceTime or iMessage, synchronizing media playback, app state, or custom data. Targets Swift 6.2 / iOS 26+.
Add the Group Activities entitlement to your app:
<key>com.apple.developer.group-session</key>
<true/>
For apps that start SharePlay without a FaceTime call (iOS 17+), add:
<key>NSSupportsGroupActivities</key>
<true/>
import GroupActivities
let observer = GroupStateObserver()
// Check if a FaceTime call or iMessage group is active
if observer.isEligibleForGroupSession {
showSharePlayButton()
}
Observe changes reactively:
for await isEligible in observer.$isEligibleForGroupSession.values {
showSharePlayButton(isEligible)
}
Conform to GroupActivity and provide metadata:
import GroupActivities
import CoreTransferable
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
}
}
| Type | Use Case |
|---|---|
.generic | Default for custom activities |
.watchTogether | Video playback |
.listenTogether | Audio playback |
.createTogether | Collaborative creation (drawing, editing) |
.workoutTogether | Shared fitness sessions |
The activity struct must conform to Codable so the system can transfer it between devices.
Set up a long-lived task to receive sessions when another participant starts the activity:
@Observable
@MainActor
final class SharePlayManager {
private var session: GroupSession<WatchTogetherActivity>?
private var messenger: GroupSessionMessenger?
private var tasks = TaskGroup()
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
Task {
for await state in session.$state.values {
handleState(state)
}
}
// Observe participant changes
Task {
for await participants in session.$activeParticipants.values {
handleParticipants(participants)
}
}
// Join the session
session.join()
}
}
| State | Description |
|---|---|
.waiting | Session exists but local participant has not joined |
.joined | Local participant is actively in the session |
.invalidated(reason:) | Session ended (check reason for details) |
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()
Use GroupSessionMessenger to sync app state between participants.
Messages must be Codable:
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) -- guaranteed delivery, ordered
let reliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .reliable
)
// Unreliable -- faster, no guarantees (good for frequent position updates)
let unreliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .unreliable
)
Use .reliable for state-changing actions (play/pause, selections). Use .unreliable for high-frequency ephemeral data (cursor positions, drawing strokes).
For video/audio, use AVPlaybackCoordinator with AVPlayer:
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)
}
Once connected, play/pause/seek actions on any participant's player are automatically synchronized to all other participants. No manual message passing is needed for playback controls.
// Notify participants about playback events
let event = GroupSessionEvent(
originator: session.localParticipant,
action: .play,
url: nil
)
session.showNotice(event)
import GroupActivities
import UIKit
func startSharePlay() async throws {
let activity = WatchTogetherActivity(
movieID: "123",
movieTitle: "Great Movie"
)
switch await activity.prepareForActivation() {
case .activationPreferred:
// Present the sharing controller
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)
case .activationDisabled:
// SharePlay is disabled or unavailable
print("SharePlay not available")
case .cancelled:
break
@unknown default:
break
}
}
For ShareLink (SwiftUI) and direct activity.activate() patterns, see references/shareplay-patterns.md.
For large data (images, files), use GroupSessionJournal instead of GroupSessionMessenger (which has a size limit):
import GroupActivities
let journal = GroupSessionJournal(session: session)
// Upload a file
let attachment = try await journal.add(imageData)
// Observe incoming attachments
Task {
for await attachments in journal.attachments {
for attachment in attachments {
let data = try await attachment.load(Data.self)
handleReceivedFile(data)
}
}
}
// WRONG -- session is received but never joined
for await session in MyActivity.sessions() {
self.session = session
// Session stays in .waiting state forever
}
// CORRECT -- join after configuring
for await session in MyActivity.sessions() {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
session.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
}
// WRONG -- messenger has a per-message size limit
let largeImage = try Data(contentsOf: imageURL) // 5 MB
try await messenger.send(largeImage, to: .all) // May fail
// CORRECT -- use GroupSessionJournal for files
let journal = GroupSessionJournal(session: session)
try await journal.add(largeImage)
// 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
// WRONG -- each time the view appears, a new listener is created
struct MyView: View {
var body: some View {
Text("Hello")
.task {
for await session in MyActivity.sessions() { }
}
}
}
// CORRECT -- observe sessions in a long-lived manager
@Observable
final class ActivityManager {
init() {
Task {
for await session in MyActivity.sessions() {
configureSession(session)
}
}
}
}
com.apple.developer.group-session) addedGroupActivity struct is Codable with meaningful metadatasessions() observed in a long-lived object (not a SwiftUI view body)session.join() called after receiving and configuring the sessionsession.leave() called when the user navigates away or dismissesGroupSessionMessenger created with appropriate deliveryMode$state and publishers observed for lifecycle changesreferences/shareplay-patterns.mdWeekly Installs
325
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex322
opencode319
github-copilot319
amp319
cline319
kimi-cli319
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
$activeParticipantsGroupSessionJournal used for large file transfers instead of messengerAVPlaybackCoordinator used for media sync (not manual messages)GroupStateObserver.isEligibleForGroupSession checked before showing SharePlay UIprepareForActivation() called before presenting sharing controller