
Shareplay Activities
- 2.5k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
shareplay-activities is an iOS skill for GroupActivities SharePlay sessions, sync, and coordinated playback.
About
SharePlay Activities documents GroupActivities framework patterns for shared real-time experiences across iOS, macOS, tvOS, and visionOS targeting Swift 6.3 and iOS 26 plus. Setup adds com.apple.developer.group-session entitlement and optional NSSupportsGroupActivities for starting sessions without an active FaceTime call on iOS 17 plus. GroupActivity structs provide Codable metadata with types like watchTogether, listenTogether, createTogether, and workoutTogether plus fallback URLs. Session lifecycle covers GroupSession joining, state synchronization, messenger send and receive, and coordinated AVPlaybackCoordinator media playback. GroupSessionJournal enables file transfers between participants. Starting SharePlay from the app uses GroupActivityActivationResult and UIActivationSession patterns when eligible per GroupStateObserver. Common mistakes address missing entitlements, non-Codable activity payloads, main-thread UI assumptions, and cleanup on session end. Review checklist sections validate eligibility checks, session error handling, and playback coordination. The skill targets FaceTime and iMessage integrated group experiences rather than generic WebRTC implementations.
- GroupActivity metadata types include watchTogether and listenTogether.
- Requires group-session entitlement and Codable activity structs.
- GroupStateObserver.isEligibleForGroupSession gates SharePlay UI.
- Coordinated media uses AVPlaybackCoordinator integration patterns.
- GroupSessionJournal supports participant file transfer flows.
Shareplay Activities by the numbers
- 2,536 all-time installs (skills.sh)
- +103 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #85 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
shareplay-activities capabilities & compatibility
- Capabilities
- groupactivity metadata and type selection · session join and messenger synchronization · coordinated avplaybackcoordinator media · eligibility observation with groupstateobserver · groupsessionjournal file transfer guidance
- Use cases
- frontend · ui design · api development
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What shareplay-activities says it does
Check if a FaceTime call or iMessage group is active
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill shareplay-activitiesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.5k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I add SharePlay for shared video or app state over FaceTime?
Implement GroupActivities SharePlay for synchronized media, collaborative state, and FaceTime-integrated sessions on Apple platforms.
Who is it for?
Apple apps adding FaceTime or iMessage integrated shared experiences.
Skip if: Skip for cross-platform WebRTC or Android group calls without GroupActivities.
When should I use this skill?
User mentions SharePlay, GroupActivities, watch together, or group session sync.
What you get
Working GroupActivity definition, session join flow, and messenger or playback coordination.
- GroupActivity Swift type
- Session lifecycle handlers
- Synchronized state messaging code
By the numbers
- Targets Swift 6.3 and iOS 26+
- Supports 4 Apple platforms: iOS, macOS, tvOS, and visionOS
Files
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.3 / 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:
// 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:
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)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 participantsDON'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
SharePlay Extended Patterns
Overflow reference for the shareplay-activities skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- Collaborative Drawing Canvas
- Full SharePlay Manager
- SwiftUI SharePlay Integration
- Custom Activity with State Sync
- Participant Tracking
Collaborative Drawing Canvas
Activity Definition
import GroupActivities
struct DrawTogetherActivity: GroupActivity {
static let activityIdentifier = "com.example.draw-together"
var metadata: GroupActivityMetadata {
var meta = GroupActivityMetadata()
meta.title = "Draw Together"
meta.type = .createTogether
return meta
}
}Stroke Message
import Foundation
struct StrokeMessage: Codable, Sendable {
let id: UUID
let points: [CGPointCodable]
let color: ColorCodable
let lineWidth: Double
struct CGPointCodable: Codable, Sendable {
let x: Double
let y: Double
}
struct ColorCodable: Codable, Sendable {
let red: Double
let green: Double
let blue: Double
let alpha: Double
}
}
struct ClearCanvasMessage: Codable, Sendable {
let timestamp: Date
}Drawing Manager
import GroupActivities
@Observable
@MainActor
final class DrawingManager {
private var session: GroupSession<DrawTogetherActivity>?
private var reliableMessenger: GroupSessionMessenger?
private var unreliableMessenger: GroupSessionMessenger?
private var tasks: [Task<Void, Never>] = []
var strokes: [StrokeMessage] = []
var isConnected = false
func startObserving() {
let task = Task {
for await session in DrawTogetherActivity.sessions() {
await configureSession(session)
}
}
tasks.append(task)
}
private func configureSession(
_ session: GroupSession<DrawTogetherActivity>
) {
// Clean up previous session
cleanUp()
self.session = session
self.reliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .reliable
)
self.unreliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .unreliable
)
// Observe state
let stateTask = Task {
for await state in session.$state.values {
switch state {
case .joined:
isConnected = true
case .invalidated:
isConnected = false
cleanUp()
default:
break
}
}
}
tasks.append(stateTask)
// Observe strokes (unreliable for speed)
let strokeTask = Task {
guard let messenger = unreliableMessenger else { return }
for await (stroke, _) in messenger.messages(of: StrokeMessage.self) {
strokes.append(stroke)
}
}
tasks.append(strokeTask)
// Observe clear messages (reliable for correctness)
let clearTask = Task {
guard let messenger = reliableMessenger else { return }
for await (_, _) in messenger.messages(of: ClearCanvasMessage.self) {
strokes.removeAll()
}
}
tasks.append(clearTask)
session.join()
}
func sendStroke(_ stroke: StrokeMessage) async {
strokes.append(stroke)
try? await unreliableMessenger?.send(stroke, to: .all)
}
func clearCanvas() async {
strokes.removeAll()
try? await reliableMessenger?.send(
ClearCanvasMessage(timestamp: Date()),
to: .all
)
}
func leave() {
session?.leave()
cleanUp()
}
private func cleanUp() {
tasks.forEach { $0.cancel() }
tasks.removeAll()
session = nil
reliableMessenger = nil
unreliableMessenger = nil
isConnected = false
}
}Full SharePlay Manager
Generic Activity Manager
import GroupActivities
@Observable
@MainActor
final class SharePlayManager<Activity: GroupActivity> {
private(set) var session: GroupSession<Activity>?
private(set) var messenger: GroupSessionMessenger?
private(set) var journal: GroupSessionJournal?
private(set) var activeParticipants: Set<Participant> = []
private(set) var localParticipant: Participant?
private(set) var isJoined = false
private var tasks: [Task<Void, Never>] = []
func startObserving() {
let task = Task {
for await session in Activity.sessions() {
await configure(session)
}
}
tasks.append(task)
}
private func configure(_ session: GroupSession<Activity>) {
reset()
self.session = session
self.messenger = GroupSessionMessenger(session: session)
self.journal = GroupSessionJournal(session: session)
self.localParticipant = session.localParticipant
let stateTask = Task {
for await state in session.$state.values {
switch state {
case .joined:
isJoined = true
case .invalidated:
isJoined = false
reset()
default:
break
}
}
}
tasks.append(stateTask)
let participantTask = Task {
for await participants in session.$activeParticipants.values {
activeParticipants = participants
}
}
tasks.append(participantTask)
session.join()
}
func leave() {
session?.leave()
reset()
}
func end() {
session?.end()
reset()
}
private func reset() {
tasks.forEach { $0.cancel() }
tasks.removeAll()
session = nil
messenger = nil
journal = nil
isJoined = false
activeParticipants = []
}
}Type-Safe Message Handling
extension SharePlayManager {
func send<T: Codable>(_ message: T) async throws {
guard let messenger else {
throw SharePlayError.notConnected
}
try await messenger.send(message, to: .all)
}
func send<T: Codable>(_ message: T, to participant: Participant) async throws {
guard let messenger else {
throw SharePlayError.notConnected
}
try await messenger.send(message, to: .only(participant))
}
func messages<T: Codable>(of type: T.Type) -> AsyncThrowingStream<(T, Participant), Error> {
AsyncThrowingStream { continuation in
let task = Task {
guard let messenger else {
continuation.finish()
return
}
for await (message, context) in messenger.messages(of: type) {
continuation.yield((message, context.source))
}
continuation.finish()
}
continuation.onTermination = { _ in task.cancel() }
}
}
}
enum SharePlayError: Error {
case notConnected
}SwiftUI SharePlay Integration
SharePlay Button
import GroupActivities
import SwiftUI
struct SharePlayButton<Activity: GroupActivity>: View {
let activity: Activity
@State private var observer = GroupStateObserver()
var body: some View {
if observer.isEligibleForGroupSession {
Button {
Task {
try await startActivity()
}
} label: {
Label("SharePlay", systemImage: "shareplay")
}
}
}
private func startActivity() async throws {
switch await activity.prepareForActivation() {
case .activationPreferred:
_ = try await activity.activate()
case .activationDisabled:
break
case .cancelled:
break
@unknown default:
break
}
}
}SharePlay Status Indicator
struct SharePlayStatusView: View {
let participantCount: Int
let isConnected: Bool
var body: some View {
if isConnected {
HStack {
Image(systemName: "shareplay")
.foregroundStyle(.green)
Text("\(participantCount) connected")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}Full Activity View
struct SharedMovieView: View {
@State private var manager = SharePlayManager<WatchTogetherActivity>()
let movieID: String
let movieTitle: String
var body: some View {
VStack {
// Movie content here
HStack {
SharePlayButton(
activity: WatchTogetherActivity(
movieID: movieID,
movieTitle: movieTitle
)
)
if manager.isJoined {
SharePlayStatusView(
participantCount: manager.activeParticipants.count,
isConnected: true
)
}
}
}
.task { manager.startObserving() }
.onDisappear { manager.leave() }
}
}Custom Activity with State Sync
Quiz Game Example
import GroupActivities
struct QuizActivity: GroupActivity {
let quizID: String
var metadata: GroupActivityMetadata {
var meta = GroupActivityMetadata()
meta.title = "Quiz Time"
meta.type = .generic
return meta
}
}
// Messages
struct QuizQuestion: Codable, Sendable {
let questionID: String
let text: String
let options: [String]
}
struct QuizAnswer: Codable, Sendable {
let questionID: String
let selectedOption: Int
}
struct QuizState: Codable, Sendable {
let currentQuestionIndex: Int
let scores: [String: Int] // participant ID -> score
}Quiz Manager
@Observable
@MainActor
final class QuizManager {
private var session: GroupSession<QuizActivity>?
private var messenger: GroupSessionMessenger?
private var tasks: [Task<Void, Never>] = []
var currentQuestion: QuizQuestion?
var scores: [String: Int] = [:]
var isHost = false
func configureSession(_ session: GroupSession<QuizActivity>) {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
self.isHost = session.isLocallyInitiated
let questionTask = Task {
guard let messenger else { return }
for await (question, _) in messenger.messages(of: QuizQuestion.self) {
currentQuestion = question
}
}
tasks.append(questionTask)
let answerTask = Task {
guard let messenger else { return }
for await (answer, context) in messenger.messages(of: QuizAnswer.self) {
processAnswer(answer, from: context.source)
}
}
tasks.append(answerTask)
// Send current state to late joiners
let participantTask = Task {
var known: Set<Participant> = []
for await participants in session.$activeParticipants.values {
let newJoiners = participants.subtracting(known)
for joiner in newJoiners {
if isHost, let question = currentQuestion {
try? await messenger?.send(question, to: .only(joiner))
}
let state = QuizState(
currentQuestionIndex: 0,
scores: scores
)
try? await messenger?.send(state, to: .only(joiner))
}
known = participants
}
}
tasks.append(participantTask)
session.join()
}
func submitAnswer(option: Int) async {
guard let question = currentQuestion else { return }
let answer = QuizAnswer(
questionID: question.questionID,
selectedOption: option
)
try? await messenger?.send(answer, to: .all)
}
private func processAnswer(_ answer: QuizAnswer, from participant: Participant) {
// Score the answer and update scores
let key = participant.id.uuidString
scores[key, default: 0] += 1
}
}Participant Tracking
Tracking Who Has Seen What
@Observable
@MainActor
final class ParticipantTracker {
private var knownParticipants: Set<Participant> = []
func handleParticipantUpdate(
_ activeParticipants: Set<Participant>,
sendStateTo: (Participant) async throws -> Void
) async {
let joined = activeParticipants.subtracting(knownParticipants)
let left = knownParticipants.subtracting(activeParticipants)
for participant in joined {
print("Participant joined: \(participant.id)")
try? await sendStateTo(participant)
}
for participant in left {
print("Participant left: \(participant.id)")
}
knownParticipants = activeParticipants
}
}Nearby Participant Detection
On iOS 17+ and visionOS, check if participants are physically nearby:
for participant in session.activeParticipants {
if participant.isNearbyWithLocalParticipant {
print("\(participant.id) is nearby")
}
}This is useful for visionOS spatial activities where you want to offer different experiences for co-located vs. remote participants.
Related skills
How it compares
Choose shareplay-activities for native FaceTime and iMessage group sync, not for custom WebRTC or cross-platform signaling stacks.
FAQ
What entitlement is required?
Add com.apple.developer.group-session to the app target.
Can activities be plain structs?
They must conform to GroupActivity and Codable for system transfer between devices.
When show SharePlay without a call?
Set NSSupportsGroupActivities and follow iOS 17 plus in-app activation patterns.
Is Shareplay Activities safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.