Claude Agent Skill · by Dpearson2699

Shareplay Activities

Install Shareplay Activities skill for Claude Code from dpearson2699/swift-ios-skills.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skills
Works 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 pack
Source file
SKILL.md490 lines
Expand
---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)