跳到主要内容
知仓学习社ZHICANG

shareplay-activities

Build shared real-time experiences using GroupActivities and SharePlay. Use when implementing shared media playback, collaborative app features, syn…

不碰外部(只输出文字)无严重或高危命中dpearson2699/swift-ios-skills

它会碰到什么

扫了多少3 个文本文件,34 KB
它会碰到什么不碰外部(只输出文字)
命中总数0 处
命中统计严重 0 · 高 0 · 中 0 · 低 0

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

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](#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

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

| Type | Use Case |

|---|---|

| .generic | Default for custom activities |

| .watchTogether | Video playback |

| .listenTogether | Audio playback |

| .createTogether | Collaborative creation (drawing, editing) |

| .exploreTogether | Shared browsing, planning, or exploration |

| .learnTogether | Shared learning or studying |

| .readTogether | Shared reading |

| .shopTogether | Shared shopping |

| .workoutTogether | Shared 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

| 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 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](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()

Configure the stored session, messenger, and observers, then call join(). The

canonical long-lived manager in Session Lifecycle shows the required order.

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

Own the sessions() listener in a long-lived manager, not a recreatable view.

Use the manager lifecycle shown above and cancel its child tasks on invalidation.

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

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。