PluginBench
Skill
Review
Audit score 70

shareplay-activities

dpearson2699/swift-ios-skills

How to install shareplay-activities

npx skills add https://github.com/dpearson2699/swift-ios-skills --skill shareplay-activities
Claude Code
Cursor
Windsurf
Cline
Full instructions (SKILL.md)

Source of truth, from dpearson2699/swift-ios-skills.


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

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. Targets Swift 6.3 / iOS 26+.

Contents

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 GroupActivitySharingController to let the user pick contacts first:

let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)

Use the shareplay SF Symbol for custom controls. Treat GroupActivityMetadata as discovery copy: concise title, subtitle, image, and type aligned with the entry point. Keep sibling domains out: GameKit owns auth, matchmaking, leaderboards, achievements, and voice/chat; TabletopKit owns seats, board equipment, spatial placement, turns, rules, and authoritative tabletop state; AVKit owns playback UI. SharePlay owns invitations, lifecycle, participants, and coordination handoffs. See references/shareplay-patterns.md for SwiftUI ShareLink, AirDrop, and direct activation patterns.

GroupSessionJournal: File Transfer

For larger, non-time-sensitive attachments, use GroupSessionJournal instead of GroupSessionMessenger. Journal items must conform to Transferable, are available to late joiners, and are limited to 100 MB. It requires iOS/iPadOS/tvOS 17+, macOS 14+, or visionOS 1+. For larger/protected assets, share a pointer or manifest and use server storage or app-managed file transfer.

import GroupActivities

let journal = GroupSessionJournal(session: session)

// Upload a Transferable file or data item
let attachment = try await journal.add(sharedImageItem)

// 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 SharePlay transports for large/protected assets

// WRONG -- messenger is small/time-sensitive; journal is Transferable and <=100 MB
let imageData = try Data(contentsOf: imageURL)     // 300 KB
try await messenger.send(imageData, to: .all)      // Too large
// CORRECT -- journal attachments up to 100 MB; otherwise share a pointer/manifest
let journal = GroupSessionJournal(session: session)
try await journal.add(sharedImageItem)
// Larger/protected assets: server storage or app-managed file transfer

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 participants

DON'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 capability added to the app target only
  • GroupActivity struct is Codable with meaningful metadata
  • Transferable conformance added when using ShareLink, AirDrop, or share sheets
  • 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 messages stay under 256 KB with appropriate deliveryMode
  • Late-joining participants receive current state on connection
  • $state and $activeParticipants publishers observed for lifecycle changes
  • GroupSessionJournal used for non-time-sensitive Transferable attachments
  • AVPlaybackCoordinator used for media sync (not manual messages)
  • GroupStateObserver.isEligibleForGroupSession checked before showing SharePlay UI
  • GroupActivitySharingController used when no conversation is active
  • Session invalidation handled with cleanup of messenger, journal, and tasks

References

Related skills

More from dpearson2699/swift-ios-skills and the wider catalog.

SW

swiftui-animation

dpearson2699/swift-ios-skills

Implement, review, or improve SwiftUI animations and transitions. Use when adding explicit animations with withAnimation, configuring implicit animations with .animation(_:body:) or .animation(_:value:), configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (iOS 17 bounce, pulse, variableColor, scale, appear, disappear, replace; iOS 18 breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.

3.0k installsAudited
IO

ios-accessibility

dpearson2699/swift-ios-skills

Implements, reviews, or improves accessibility in iOS/macOS apps with SwiftUI, UIKit, and AppKit. Use when adding VoiceOver, Voice Control, Switch Control, or Full Keyboard Access support; when working with accessibility labels, hints, values, traits, accessibilityInputLabels, NSAccessibility, grouping, reading order, accessibility focus restoration with @AccessibilityFocusState, Dynamic Type, @ScaledMetric, custom rotors, accessibility actions, XCTest accessibility checks, App Store Accessibility Nutrition Labels, App Store Connect accessibility answers, a11y compliance audits, or system accessibility preferences.

2.7k installsAudited
SW

swiftui-patterns

dpearson2699/swift-ios-skills

Builds and reviews SwiftUI views with modern MV architecture, state management, view composition, and migration/availability guidance. Covers @Observable ownership rules, @State/@Bindable/@Environment wiring, view decomposition, custom ViewModifiers, environment values, async data loading with .task, iOS 26+ handoff reminders, Writing Tools, clipboard availability caveats, and performance guidelines. Use when structuring SwiftUI app state, managing @Observable, composing view hierarchies, or correcting SwiftUI pattern guidance.

2.6k installsAudited
SW

swiftui-performance

dpearson2699/swift-ios-skills

Audit and improve SwiftUI runtime performance. Use when diagnosing slow rendering, janky scrolling, high CPU, memory usage, excessive view updates, layout thrash, body evaluation cost, identity churn, view lifetime issues, lazy loading, Instruments profiling guidance, and performance audit requests.

2.6k installsAudited
AP

app-store-review

dpearson2699/swift-ios-skills

Prepare for App Store review and prevent rejections. Covers App Store review guidelines, app rejection reasons, PrivacyInfo.xcprivacy privacy manifest requirements, required API reason codes, in-app purchase IAP and StoreKit rules, App Store Guidelines compliance, ATT App Tracking Transparency, EU DMA Digital Markets Act, HIG compliance checklist, app submission preparation, review preparation, metadata requirements, entitlements, widgets, and Live Activities review rules. Use when preparing for App Store submission, fixing rejection reasons, auditing privacy manifests, implementing ATT consent flow, configuring StoreKit IAP, or checking HIG compliance.

2.5k installs
IO

ios-networking

dpearson2699/swift-ios-skills

Build, review, or improve networking code in iOS/macOS apps using URLSession with async/await, structured concurrency, and modern Swift patterns. Use when working with REST APIs, downloading files, uploading data, WebSocket connections, pagination, retry logic, request middleware, caching, background transfers, or network reachability monitoring. Also use when handling HTTP requests, API clients, network error handling, or data fetching in Swift apps.

2.5k installs