shareplay-activities — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited shareplay-activities (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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+.
Add the Group Activities entitlement to your app:
<key>com.apple.developer.group-session</key>
<true/>For apps that start SharePlay without a FaceTime call (iOS 17+), add:
<key>NSSupportsGroupActivities</key>
<true/>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)
}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
}
}| 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.
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()
}
}| 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) |
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)")
}// Leave the session (other participants continue)
session?.leave()
// End the session for all participants
session?.end()Use GroupSessionMessenger to sync app state between participants.
Messages must be Codable:
struct SyncMessage: Codable {
let action: String
let timestamp: Date
let data: [String: String]
}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))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)
}
}
}// 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).
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.
// Notify participants about playback events
let event = GroupSessionEvent(
originator: session.localParticipant,
action: .play,
url: nil
)
session.showNotice(event)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.
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)
}
}
}// 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()
}// 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
}// 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
}// 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)// 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// 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)
}
}
}
}com.apple.developer.group-session) addedGroupActivity struct is Codable with meaningful metadatasessions() observed in a long-lived object (not a SwiftUI view body)session.join() called after receiving and configuring the sessionsession.leave() called when the user navigates away or dismissesGroupSessionMessenger created with appropriate deliveryMode$state and $activeParticipants publishers observed for lifecycle changesGroupSessionJournal used for large file transfers instead of messengerAVPlaybackCoordinator used for media sync (not manual messages)GroupStateObserver.isEligibleForGroupSession checked before showing SharePlay UIprepareForActivation() called before presenting sharing controller~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.