--- 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](#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 com.apple.developer.group-session ``` ### Info.plist For apps that start SharePlay without a FaceTime call (iOS 17+), add: ```xml NSSupportsGroupActivities ``` ### Checking Eligibility ```swift import GroupActivities let observer = GroupStateObserver() // Check if a FaceTime call or iMessage group is active if observer.isEligibleForGroupSession { showSharePlayButton() } ``` Observe changes reactively: ```swift for await isEligible in observer.$isEligibleForGroupSession.values { showSharePlayButton(isEligible) } ``` ## Defining a GroupActivity Conform to `GroupActivity` and provide metadata: ```swift 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: ```swift @Observable @MainActor final class SharePlayManager { private var session: GroupSession? 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 ) { 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 ```swift private func handleState(_ state: GroupSession.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) { print("Active participants: \(participants.count)") } ``` ### Leaving and Ending ```swift // 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`: ```swift struct SyncMessage: Codable { let action: String let timestamp: Date let data: [String: String] } ``` ### Sending ```swift 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 ```swift 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 ```swift // 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`: ```swift import AVFoundation import GroupActivities func configurePlayback( session: GroupSession, 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 ```swift // 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) ```swift 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): ```swift 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() ```swift // 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 ```swift // 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 ```swift // 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) { 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 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 ```swift // 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 ```swift // 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 - [ ] `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` - [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)