shareplay-activities
"Build shared real-time experiences using GroupActivities and SharePlay. Use when implementing shared media playback, collaborative app features, synchronized game state, or any FaceTime/iMessage-integrated group activity on iOS, macOS, tvOS, or visionOS."
npx skills add dpearson2699/swift-ios-skills --skill shareplay-activitiesBefore / After 效果对比
1 组1在没有 GroupActivities 和 SharePlay 框架的情况下,构建实时共享体验(如同步媒体播放、协作应用功能)需要开发者自行实现复杂的网络同步逻辑、状态管理和用户界面协调。这通常涉及大量的后端开发和客户端同步代码,且难以与 FaceTime/iMessage 等系统级功能集成。
2
3```swift
4// 假设通过自定义后端和 WebSocket 实现同步播放
5// class CustomMediaSynchronizer {
6// func startSession(mediaID: String) {
7// // ... 连接 WebSocket
8// // ... 发送播放/暂停/进度更新事件到后端
9// // ... 接收并应用其他用户的事件
10// }
11// }
12print("手动构建实时共享体验复杂且难以与系统集成。")
13```1使用 GroupActivities 和 SharePlay 框架,开发者可以轻松构建与 FaceTime/iMessage 深度集成的实时共享体验。它提供了标准化的 API 来管理共享活动、同步媒体播放状态、共享应用数据和协调用户界面,大大简化了协作功能的开发。
2
3```swift
4import GroupActivities
5import SwiftUI
6
7// 定义一个可共享的活动
8struct MySharedActivity: GroupActivity {
9 static let activityIdentifier = "com.example.myapp.mysharedactivity"
10
11 var metadata: GroupActivityMetadata {
12 var metadata = GroupActivityMetadata()
13 metadata.title = "共享观看我的视频"
14 metadata.fallbackURL = URL(string: "https://example.com/fallback")
15 metadata.supportsContinuationAndSuspending = true
16 return metadata
17 }
18
19 // 启动活动
20 func activate() async {
21 do {
22 let activationHandle = try await self.activate(options: .init())
23 // 处理激活句柄,例如开始同步媒体播放
24 print("活动已激活,会话 ID: \(activationHandle.id)")
25 } catch {
26 print("激活活动失败: \(error)")
27 }
28 }
29
30 // 参与活动
31 static func join(activity: MySharedActivity) async {
32 for await session in activity.sessions() {
33 // 处理会话,例如同步媒体播放器状态
34 print("已加入会话: \(session.id)")
35 // 监听会话状态变化和消息
36 Task { await handleSession(session) }
37 }
38 }
39
40 static func handleSession(_ session: GroupSession) async {
41 // 示例:同步媒体播放器状态
42 // let messenger = GroupSessionMessenger(session: session)
43 // for await (message, context) in messenger.messages(of: PlaybackState.self) {
44 // // 更新本地播放器状态
45 // }
46 print("处理会话数据同步...")
47 }
48}
49
50// MARK: - 启动共享活动 (UI 示例)
51struct ContentView: View {
52 @State private var isSharing = false
53
54 var body: some View {
55 VStack {
56 Button("开始共享活动") {
57 Task {
58 await MySharedActivity().activate()
59 }
60 }
61 .disabled(isSharing)
62 }
63 .onAppear {
64 // 监听是否有活动正在进行
65 Task {
66 for await session in MySharedActivity.sessions() {
67 isSharing = true
68 await MySharedActivity.handleSession(session)
69 }
70 isSharing = false
71 }
72 }
73 }
74}
75```description SKILL.md
name: shareplay-activities description: "Build shared real-time experiences using GroupActivities and SharePlay. Use when implementing shared media playback, collaborative app features, synchronized game state, or any FaceTime/iMessage-integrated group activity on iOS, macOS, tvOS, or visionOS."
GroupActivities / SharePlay
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+.
Contents
- Setup
- Defining a GroupActivity
- Session Lifecycle
- Sending and Receiving Messages
- Coordinated Media Playback
- Starting SharePlay from Your App
- GroupSessionJournal: File Transfer
- Common Mistakes
- Review Checklist
- References
Setup
Entitlements
Add the Group Activities entitlement to your app:
<key>com.apple.developer.group-session</key>
<true/>
Info.plist
For apps that start SharePlay without a FaceTime call (iOS 17+), add:
<key>NSSupportsGroupActivities</key>
<true/>
Checking Eligibility
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)
}
Defining a GroupActivity
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
}
}
Activity Types
| 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.
Session Lifecycle
Listening for Sessions
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()
}
}
Session States
| 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) |
Handling State Changes
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)")
}
Leaving and Ending
// Leave the session (other participants continue)
session?.leave()
// End the session for all participants
session?.end()
Sending and Receiving Messages
Use GroupSessionMessenger to sync app state between participants.
Defining Messages
Messages must be Codable:
struct SyncMessage: Codable {
let action: String
let timestamp: Date
let data: [String: String]
}
Sending
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))
Receiving
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)
}
}
}
Delivery Modes
// 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).
Coordinated Media Playback
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.
Handling Playback Events
// Notify participants about playback events
let event = GroupSessionEvent(
originator: session.localParticipant,
action: .play,
url: nil
)
session.showNotice(event)
Starting SharePlay from Your App
Using GroupActivitySharingController (UIKit)
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.
GroupSessionJournal: File Transfer
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)
}
}
}
Common Mistakes
DON'T: Forget to call session.join()
// 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()
}
DON'T: Forget to leave or end sessions
// 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
}
DON'T: Assume all participants have the same state
// 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
}
DON'T: Use GroupSessionMessenger for large data
// 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)
DON'T: Send redundant messages for media playback
// 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
DON'T: Observe sessions in a view that gets recreated
// 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)
}
}
}
}
Review Checklist
- Group Activities entitlement (
com.apple.developer.group-session) added -
GroupActivitystruct isCodablewith meaningful metadata -
sessions()observed in a long-lived object (not a SwiftUI view body) -
session.join()called after receiving and configuring the session -
session.leave()called when the user navigates away or dismisses -
GroupSessionMessengercreated with appropriatedeliveryMode - Late-joining participants receive current state on connection
-
$stateand$activeParticipantspublishers observed for lifecycle changes -
GroupSessionJournalused for large file transfers instead of messenger -
AVPlaybackCoordinatorused for media sync (not manual messages) -
GroupStateObserver.isEligibleForGroupSessionchecked before showing SharePlay UI -
prepareForActivation()called before presenting sharing controller - Session invalidation handled with cleanup of messenger, journal, and tasks
References
- Extended patterns (collaborative canvas, spatial Personas, custom templates):
references/shareplay-patterns.md - GroupActivities framework
- GroupActivity protocol
- GroupSession
- GroupSessionMessenger
- GroupSessionJournal
- GroupStateObserver
- GroupActivitySharingController
- Defining your app's SharePlay activities
- Presenting SharePlay activities from your app's UI
- Synchronizing data during a SharePlay activity
forum用户评价 (0)
发表评价
暂无评价,来写第一条吧
统计数据
用户评分
为此 Skill 评分