dpearson2699/swift-ios-skills3 files

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, Messages, AirDrop, or nearby visionOS group activity on iOS, macOS, tvOS, or visionOS.

Specification
Skill ID
dpearson2699/swift-ios-skills/shareplay-activities
Publisher
dpearson2699
Repository
swift-ios-skills
Installs
0
Files
3
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

dpearson2699/swift-ios-skills/shareplay-activitiesInstalls these files
  • SKILL.md
  • evals/evals.json
  • references/shareplay-patterns.md

What this skill tells the agent

GroupActivities / SharePlay

Build shared real-time experiences using the GroupActivities framework. SharePlay connects people over FaceTime, Messages, AirDrop, and nearby visionOS sharing, synchronizing media playback, app state, or custom data.

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

Capability

Add the Group Activities capability to the app target in Xcode. Xcode adds the required entitlement and updates the provisioning profile:

<key>com.apple.developer.group-session</key>
<true/>

Configure this only for app targets. Group Activities are not available in widgets, extensions, or App Clips.

Checking Eligibility

import GroupActivities

let observer = GroupStateObserver()

// Check if a FaceTime call or Messages conversation 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

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

TypeUse Case
.genericDefault for custom activities
.watchTogetherVideo playback
.listenTogetherAudio playback
.createTogetherCollaborative creation (drawing, editing)
.exploreTogetherShared browsing, planning, or exploration
.learnTogetherShared learning or studying
.readTogetherShared reading
.shopTogetherShared shopping
.workoutTogetherShared fitness sessions

GroupActivity is Codable; stored activity data must be codable. Add Transferable only for SwiftUI ShareLink, SharePlay over AirDrop, or AppKit/UIKit share sheets. Keep payloads minimal: use identifiers or URLs instead of large data.

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 sessionTasks: [Task<Void, Never>] = []

    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
        let stateTask = Task {
            for await state in session.$state.values {
                handleState(state)
            }
        }
        sessionTasks.append(stateTask)

        // Observe participant changes
        let participantTask = Task {
            for await participants in session.$activeParticipants.values {
                handleParticipants(participants)
            }
        }
        sessionTasks.append(participantTask)

        // Join the session
        session.join()
    }

    private func cleanUp() {
        sessionTasks.forEach { $0.cancel() }
        sessionTasks.removeAll()
        session = nil
        messenger = nil
    }
}

Session States

StateDescription
.waitingSession exists but local participant has not joined
.joinedLocal 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 small, time-sensitive app state between participants.

Defining Messages

Messages must be Codable; keep each message under 256 KB.

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) -- checked and retried for crucial state
let reliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .reliable
)

// Unreliable -- lower latency, no delivery guarantee
let unreliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .unreliable
)

Use .reliable for state-changing actions such as selections or turns. Use .unreliable for high-frequency ephemeral data such as cursor positions, drawing strokes, and reactions.

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, AVFoundation synchronizes play/pause, seeking, rate, playback speed, and time. Do not put AVPlayer transport fields in messenger messages or snapshots, including late-joiner snapshots; use custom messages only for state outside playback.

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:
        // A conversation is active and the user chose to share.
        _ = try await activity.activate()

    case .activationDisabled:
        // The user chose local playback, or sharing is unavailable.
        startLocalExperience()

    case .cancelled:
        break

    @unknown default:
        break
    }
}

When no conversation is active (i.e., isEligibleForGroupSession is false),

Use this skill in a project

Open any RiverX project, search the Skills panel for dpearson2699/swift-ios-skills/shareplay-activities, and install it in one click.

Use this skill