Shareplay Activities

作者 dpearson26998d90fd121a26無授權條款1.1K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫2 個月前更新

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.

AI 產生的概覽

指導在 Apple 平台上使用 GroupActivities 與 SharePlay 實作同步共享體驗。

功能
提供使用 Apple GroupActivities 框架建構共享即時體驗的參考說明,涵蓋能力設定、GroupActivity 定義、工作階段生命週期、訊息傳遞、協調媒體播放與檔案傳輸。包含程式碼模式、常見錯誤與審查清單。產出的是實作指引,而非可執行的成品。
適用情境
適用於在 iOS、macOS、tvOS 或 visionOS 上實作 SharePlay 或 GroupActivities 功能,例如共享媒體播放、協作應用程式狀態或同步群組活動。也適合對照常見陷阱審查現有的 SharePlay 實作。
執行需求
無指令碼,僅為說明文件。需要 Apple 平台開發環境,包含 Xcode,並為目標應用程式設定 Group Activities 能力/權限。

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

Capability

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

xml
<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

swift
import GroupActivities
let observer = GroupStateObserver()
// Check if a FaceTime call or Messages conversation is activeif observer.isEligibleForGroupSession {    showSharePlayButton()}

Observe changes reactively:

swift
for await isEligible in observer.$isEligibleForGroupSession.values {    showSharePlayButton(isEligible)}

Defining a GroupActivity

Conform to GroupActivity and provide metadata:

swift
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:

swift
@Observable@MainActorfinal 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

swift
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

swift
// Leave the session (other participants continue)session?.leave()
// End the session for all participantssession?.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.

swift
struct SyncMessage: Codable {    let action: String    let timestamp: Date    let data: [String: String]}

Sending

swift
func sendSync(_ message: SyncMessage) async throws {    guard let messenger else { return }
    try await messenger.send(message, to: .all)}
// Send to specific participantstry await messenger.send(message, to: .only(participant))

Receiving

swift
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

swift
// Reliable (default) -- checked and retried for crucial statelet reliableMessenger = GroupSessionMessenger(    session: session,    deliveryMode: .reliable)
// Unreliable -- lower latency, no delivery guaranteelet 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:

swift
import AVFoundationimport 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)

swift
import GroupActivitiesimport 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:

swift
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 [blocked] 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.

swift
import GroupActivities
let journal = GroupSessionJournal(session: session)
// Upload a Transferable file or data itemlet attachment = try await journal.add(sharedImageItem)
// Observe incoming attachmentsTask {    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

swift
// WRONG -- session stays alive after the user navigates awayfunc viewDidDisappear() {    // Nothing -- session leaks}
// CORRECT -- leave when the view is dismissedfunc viewDidDisappear() {    session?.leave()    session = nil    messenger = nil}

DON'T: Assume all participants have the same state

swift
// WRONG -- broadcasting state without handling late joinersfunc onJoin() {    // New participant has no idea what the current state is}
// CORRECT -- send full state to new participantsfunc 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

swift
// WRONG -- messenger is small/time-sensitive; journal is Transferable and <=100 MBlet imageData = try Data(contentsOf: imageURL)     // 300 KBtry await messenger.send(imageData, to: .all)      // Too large// CORRECT -- journal attachments up to 100 MB; otherwise share a pointer/manifestlet 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

swift
// WRONG -- manually syncing play/pause when using AVPlayerfunc play() {    player.play()    try await messenger.send(PlayMessage(), to: .all)}
// CORRECT -- let AVPlaybackCoordinator handle itplayer.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

來源與署名

來源:dpearson2699/swift-ios-skills位於skills/shareplay-activities提交8d90fd1

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架

更多來自 dpearson2699/swift-ios-skills 的技能

Widgetkit

dpearson2699

指導實作、審查與改進 iOS、iPadOS、watchOS 與 CarPlay 上的 WidgetKit 小工具與控制項。

Software Development1.1K2 個月前更新

Weatherkit

dpearson2699

指導 iOS 開發者使用 WeatherService 取得 WeatherKit 預報、警報與署名資訊。

Software Development1.1K2 個月前更新

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

待分類1.1K2 個月前更新

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

待分類1.1K2 個月前更新

Tabletopkit

dpearson2699

指導使用 TabletopKit 在 visionOS 上打造多人空間桌遊,涵蓋棋具、座位、動作與 RealityKit 算繪。

Software Development1.1K2 個月前更新

Swiftui Webkit

dpearson2699

指導在 iOS 26 及更新版本的 SwiftUI App 中使用 WebKit for SwiftUI 嵌入與控制網頁內容。

Software Development1.1K2 個月前更新