Install
Terminal · npx$
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skillsWorks with Paperclip
How Shareplay Activities fits into a Paperclip company.
Shareplay Activities drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
S
SaaS FactoryPaired
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
$27$59
Explore packSource file
SKILL.md490 linesExpandCollapse
---name: shareplay-activitiesdescription: "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. SharePlayconnects people over FaceTime or iMessage, synchronizing media playback, app state,or custom data. Targets Swift 6.3 / iOS 26+. ## Contents - [Setup](#setup)- [Defining a GroupActivity](#defining-a-groupactivity)- [Session Lifecycle](#session-lifecycle)- [Sending and Receiving Messages](#sending-and-receiving-messages)- [Coordinated Media Playback](#coordinated-media-playback)- [Starting SharePlay from Your App](#starting-shareplay-from-your-app)- [GroupSessionJournal: File Transfer](#groupsessionjournal-file-transfer)- [Common Mistakes](#common-mistakes)- [Review Checklist](#review-checklist)- [References](#references) ## Setup ### Entitlements Add the Group Activities entitlement to your app: ```xml<key>com.apple.developer.group-session</key><true/>``` ### Info.plist For apps that start SharePlay without a FaceTime call (iOS 17+), add: ```xml<key>NSSupportsGroupActivities</key><true/>``` ### Checking Eligibility ```swiftimport GroupActivities let observer = GroupStateObserver() // Check if a FaceTime call or iMessage group is activeif observer.isEligibleForGroupSession { showSharePlayButton()}``` Observe changes reactively: ```swiftfor await isEligible in observer.$isEligibleForGroupSession.values { showSharePlayButton(isEligible)}``` ## Defining a GroupActivity Conform to `GroupActivity` and provide metadata: ```swiftimport GroupActivitiesimport 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 itbetween devices. ## Session Lifecycle ### Listening for Sessions Set up a long-lived task to receive sessions when another participant startsthe activity: ```swift@Observable@MainActorfinal 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 ```swiftprivate 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 ```swift// Leave the session (other participants continue)session?.leave() // End the session for all participantssession?.end()``` ## Sending and Receiving Messages Use `GroupSessionMessenger` to sync app state between participants. ### Defining Messages Messages must be `Codable`: ```swiftstruct SyncMessage: Codable { let action: String let timestamp: Date let data: [String: String]}``` ### Sending ```swiftfunc sendSync(_ message: SyncMessage) async throws { guard let messenger else { return } try await messenger.send(message, to: .all)} // Send to specific participantstry await messenger.send(message, to: .only(participant))``` ### Receiving ```swiftfunc 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 ```swift// Reliable (default) -- guaranteed delivery, orderedlet 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`: ```swiftimport AVFoundationimport 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 areautomatically synchronized to all other participants. No manual messagepassing is needed for playback controls. ### Handling Playback Events ```swift// Notify participants about playback eventslet event = GroupSessionEvent( originator: session.localParticipant, action: .play, url: nil)session.showNotice(event)``` ## Starting SharePlay from Your App ### Using GroupActivitySharingController (UIKit) ```swiftimport GroupActivitiesimport UIKit func startSharePlay() async throws { let activity = WatchTogetherActivity( movieID: "123", movieTitle: "Great Movie" ) switch await activity.prepareForActivation() { case .activationPreferred: // Already in a FaceTime/iMessage session — activate directly _ = try await activity.activate() case .activationDisabled: // SharePlay is disabled or unavailable print("SharePlay not available") case .cancelled: break @unknown default: break }}``` When no conversation is active (i.e., `isEligibleForGroupSession` is false),use `GroupActivitySharingController` to let the user pick contacts first: ```swiftlet controller = try GroupActivitySharingController(activity)present(controller, animated: true)``` For `ShareLink` (SwiftUI) and direct `activity.activate()` patterns, see[references/shareplay-patterns.md](references/shareplay-patterns.md). ## GroupSessionJournal: File Transfer For large data (images, files), use `GroupSessionJournal` instead of`GroupSessionMessenger` (which has a size limit): ```swiftimport GroupActivities let journal = GroupSessionJournal(session: session) // Upload a filelet attachment = try await journal.add(imageData) // Observe incoming attachmentsTask { 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() ```swift// WRONG -- session is received but never joinedfor await session in MyActivity.sessions() { self.session = session // Session stays in .waiting state forever} // CORRECT -- join after configuringfor await session in MyActivity.sessions() { self.session = session self.messenger = GroupSessionMessenger(session: session) session.join()}``` ### DON'T: Forget to leave or end sessions ```swift// WRONG -- session stays alive after the user navigates awayfunc viewDidDisappear() { // Nothing -- session leaks} // CORRECT -- leave when the view is dismissedfunc viewDidDisappear() { session?.leave() session = nil messenger = nil}``` ### DON'T: Assume all participants have the same state ```swift// WRONG -- broadcasting state without handling late joinersfunc onJoin() { // New participant has no idea what the current state is} // CORRECT -- send full state to new participantsfunc 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 ```swift// WRONG -- messenger has a per-message size limitlet largeImage = try Data(contentsOf: imageURL) // 5 MBtry await messenger.send(largeImage, to: .all) // May fail // CORRECT -- use GroupSessionJournal for fileslet journal = GroupSessionJournal(session: session)try await journal.add(largeImage)``` ### DON'T: Send redundant messages for media playback ```swift// WRONG -- manually syncing play/pause when using AVPlayerfunc play() { player.play() try await messenger.send(PlayMessage(), to: .all)} // CORRECT -- let AVPlaybackCoordinator handle itplayer.playbackCoordinator.coordinateWithSession(session)player.play() // Automatically synced to all participants``` ### DON'T: Observe sessions in a view that gets recreated ```swift// WRONG -- each time the view appears, a new listener is createdstruct MyView: View { var body: some View { Text("Hello") .task { for await session in MyActivity.sessions() { } } }} // CORRECT -- observe sessions in a long-lived manager@Observablefinal class ActivityManager { init() { Task { for await session in MyActivity.sessions() { configureSession(session) } } }}``` ## Review Checklist - [ ] Group Activities entitlement (`com.apple.developer.group-session`) added- [ ] `GroupActivity` struct is `Codable` with 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- [ ] `GroupSessionMessenger` created with appropriate `deliveryMode`- [ ] Late-joining participants receive current state on connection- [ ] `$state` and `$activeParticipants` publishers observed for lifecycle changes- [ ] `GroupSessionJournal` used for large file transfers instead of messenger- [ ] `AVPlaybackCoordinator` used for media sync (not manual messages)- [ ] `GroupStateObserver.isEligibleForGroupSession` checked 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](references/shareplay-patterns.md)- [GroupActivities framework](https://sosumi.ai/documentation/groupactivities)- [GroupActivity protocol](https://sosumi.ai/documentation/groupactivities/groupactivity)- [GroupSession](https://sosumi.ai/documentation/groupactivities/groupsession)- [GroupSessionMessenger](https://sosumi.ai/documentation/groupactivities/groupsessionmessenger)- [GroupSessionJournal](https://sosumi.ai/documentation/groupactivities/groupsessionjournal)- [GroupStateObserver](https://sosumi.ai/documentation/groupactivities/groupstateobserver)- [GroupActivitySharingController](https://sosumi.ai/documentation/groupactivities/groupactivitysharingcontroller-ybcy)- [Defining your app's SharePlay activities](https://sosumi.ai/documentation/groupactivities/defining-your-apps-shareplay-activities)- [Presenting SharePlay activities from your app's UI](https://sosumi.ai/documentation/groupactivities/promoting-shareplay-activities-from-your-apps-ui)- [Synchronizing data during a SharePlay activity](https://sosumi.ai/documentation/groupactivities/synchronizing-data-during-a-shareplay-activity)