shareplay-activities

shareplay-activities

热门

使用 GroupActivities 和 SharePlay 构建共享的实时体验。适用于实现共享媒体播放、协作应用功能、同步游戏状态,或任何在 iOS、macOS、tvOS 或 visionOS 上通过 FaceTime、信息、隔空投送或附近 visionOS 共享进行的群组活动。

940Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
shareplay-activities
description

使用 GroupActivities 和 SharePlay 构建共享的实时体验。适用于实现共享媒体播放、协作应用功能、同步游戏状态,或任何在 iOS、macOS、tvOS 或 visionOS 上通过 FaceTime、信息、隔空投送或附近 visionOS 共享进行的群组活动。

GroupActivities / SharePlay

使用 GroupActivities 框架构建共享的实时体验。SharePlay 通过 FaceTime、信息、隔空投送和附近 visionOS 共享连接用户,同步媒体播放、应用状态或自定义数据。

目录

设置

功能

在 Xcode 中为应用目标添加 Group Activities 功能。Xcode 会添加所需的授权并更新配置文件:

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

仅对应用目标进行配置。Group Activities 在小组件、扩展或 App Clips 中不可用。

检查资格

import GroupActivities

let observer = GroupStateObserver()

// 检查是否有活跃的 FaceTime 通话或信息对话
if observer.isEligibleForGroupSession {
    showSharePlayButton()
}

响应式观察变化:

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

定义 GroupActivity

遵循 GroupActivity 协议并提供元数据:

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

活动类型

类型 用例
.generic 自定义活动的默认类型
.watchTogether 视频播放
.listenTogether 音频播放
.createTogether 协作创作(绘图、编辑)
.exploreTogether 共享浏览、规划或探索
.learnTogether 共享学习或研究
.readTogether 共享阅读
.shopTogether 共享购物
.workoutTogether 共享健身会话

GroupActivity 遵循 Codable;存储的活动数据必须是可编码的。仅在使用 SwiftUI ShareLink、通过隔空投送进行 SharePlay 或 AppKit/UIKit 共享表单时添加 Transferable。保持负载最小:使用标识符或 URL 而不是大数据。

会话生命周期

监听会话

设置一个长期运行的任务,以便在其他参与者启动活动时接收会话:

@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)

        // 观察会话状态变化
        let stateTask = Task {
            for await state in session.$state.values {
                handleState(state)
            }
        }
        sessionTasks.append(stateTask)

        // 观察参与者变化
        let participantTask = Task {
            for await participants in session.$activeParticipants.values {
                handleParticipants(participants)
            }
        }
        sessionTasks.append(participantTask)

        // 加入会话
        session.join()
    }

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

会话状态

状态 描述
.waiting 会话存在但本地参与者尚未加入
.joined 本地参与者已活跃在会话中
.invalidated(reason:) 会话结束(查看原因获取详情)

处理状态变化

private func handleState(_ state: GroupSession<WatchTogetherActivity>.State) {
    switch state {
    case .waiting:
        print("等待加入")
    case .joined:
        print("已加入会话")
        loadActivity(session?.activity)
    case .invalidated(let reason):
        print("会话结束:\(reason)")
        cleanUp()
    @unknown default:
        break
    }
}

private func handleParticipants(_ participants: Set<Participant>) {
    print("活跃参与者:\(participants.count)")
}

离开和结束

// 离开会话(其他参与者继续)
session?.leave()

// 结束所有参与者的会话
session?.end()

发送和接收消息

使用 GroupSessionMessenger 在参与者之间同步小型、时间敏感的应用状态。

定义消息

消息必须遵循 Codable;每条消息保持在 256 KB 以下。

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)
}

// 发送给特定参与者
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)
        }
    }
}

传递模式

// 可靠(默认)——对关键状态进行校验和重试
let reliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .reliable
)

// 不可靠——低延迟,不保证送达
let unreliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .unreliable
)

对于改变状态的操作(如选择、回合),使用 .reliable。对于高频、短暂的数据(如光标位置、绘图笔触、反应),使用 .unreliable

协调媒体播放

对于视频/音频,使用 AVPlaybackCoordinatorAVPlayer

import AVFoundation
import GroupActivities

func configurePlayback(
    session: GroupSession<WatchTogetherActivity>,
    player: AVPlayer
) {
    // 将播放器的协调器连接到会话
    let coordinator = player.playbackCoordinator
    coordinator.coordinateWithSession(session)
}

连接后,AVFoundation 会同步播放/暂停、跳转、速率、播放速度和时间。不要将 AVPlayer 传输字段放入 messenger 消息或快照(包括后加入者快照)中;仅对播放之外的状态使用自定义消息。

从应用启动 SharePlay

使用 GroupActivitySharingController (UIKit)

import GroupActivities
import UIKit

func startSharePlay() async throws {
    let activity = WatchTogetherActivity(
        movieID: "123",
        movieTitle: "Great Movie"
    )

    switch await activity.prepareForActivation() {
    case .activationPreferred:
        // 有活跃对话且用户选择共享。
        _ = try await activity.activate()

    case .activationDisabled:
        // 用户选择本地播放,或共享不可用。
        startLocalExperience()

    case .cancelled:
        break

    @unknown default:
        break
    }
}

当没有活跃对话时(即 isEligibleForGroupSession 为 false),使用 GroupActivitySharingController 让用户先选择联系人:

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

为自定义控件使用 shareplay SF 符号。将 GroupActivityMetadata 视为发现文案:简洁的标题、副标题、图像和类型,与入口点一致。保持领域分离:GameKit 拥有认证、匹配、排行榜、成就和语音/聊天;TabletopKit 拥有座位、棋盘设备、空间放置、回合、规则和权威桌面状态;AVKit 拥有播放 UI。SharePlay 拥有邀请、生命周期、参与者和协调交接。有关 SwiftUI ShareLink、隔空投送和直接激活模式,请参阅 references/shareplay-patterns.md

GroupSessionJournal:文件传输

对于较大、非时间敏感的附件,使用 GroupSessionJournal 而不是 GroupSessionMessenger。日志项必须遵循 Transferable,对后加入者可用,且限制为 100 MB。需要 iOS/iPadOS/tvOS 17+、macOS 14+ 或 visionOS 1+。对于更大/受保护的资源,共享指针或清单,并使用服务器存储或应用管理的文件传输。

import GroupActivities

let journal = GroupSessionJournal(session: session)

// 上传一个 Transferable 文件或数据项
let attachment = try await journal.add(sharedImageItem)

// 观察传入的附件
Task {
    for await attachments in journal.attachments {
        for attachment in attachments {
            let data = try await attachment.load(Data.self)
            handleReceivedFile(data)
        }
    }
}

常见错误

不要:忘记调用 session.join()

配置好存储的会话、messenger 和观察者后,调用 join()。会话生命周期中的规范长期管理器显示了所需的顺序。

不要:忘记离开或结束会话

// 错误——用户导航离开后会话仍然存活
func viewDidDisappear() {
    // 什么都不做——会话泄漏
}

// 正确——视图消失时离开
func viewDidDisappear() {
    session?.leave()
    session = nil
    messenger = nil
}

不要:假设所有参与者状态相同

// 错误——广播状态但不处理后加入者
func onJoin() {
    // 新参与者不知道当前状态
}

// 正确——向新参与者发送完整状态
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
}

不要:使用 SharePlay 传输大型/受保护资源

// 错误——messenger 用于小型/时间敏感数据;journal 是 Transferable 且 <=100 MB
let imageData = try Data(contentsOf: imageURL)     // 300 KB
try await messenger.send(imageData, to: .all)      // 太大
// 正确——journal 附件最大 100 MB;否则共享指针/清单
let journal = GroupSessionJournal(session: session)
try await journal.add(sharedImageItem)
// 更大/受保护资源:服务器存储或应用管理的文件传输

不要:为媒体播放发送冗余消息

// 错误——使用 AVPlayer 时手动同步播放/暂停
func play() {
    player.play()
    try await messenger.send(PlayMessage(), to: .all)
}

// 正确——让 AVPlaybackCoordinator 处理
player.playbackCoordinator.coordinateWithSession(session)
player.play()  // 自动同步给所有参与者

不要:在会重建的视图中观察会话

sessions() 监听器放在长期运行的管理器中,而不是可重建的视图中。使用上面显示的管理器生命周期,并在失效时取消其子任务。

审查清单

  • [ ] 仅对应用目标添加了 Group Activities 功能
  • [ ] GroupActivity 结构体遵循 Codable 并包含有意义的元数据
  • [ ] 使用 ShareLink、隔空投送或共享表单时添加了 Transferable 遵循
  • [ ] sessions() 在长期对象中观察(而非 SwiftUI 视图体)
  • [ ] 接收并配置会话后调用了 session.join()
  • [ ] 用户导航离开或关闭时调用了 session.leave()
  • [ ] GroupSessionMessenger 消息保持在 256 KB 以下,并使用适当的 deliveryMode
  • [ ] 后加入的参与者在连接时接收当前状态
  • [ ] 观察 $state$activeParticipants 发布者以处理生命周期变化
  • [ ] 对于非时间敏感的 Transferable 附件使用 GroupSessionJournal
  • [ ] 使用 AVPlaybackCoordinator 进行媒体同步(而非手动消息)
  • [ ] 在显示 SharePlay UI 前检查 GroupStateObserver.isEligibleForGroupSession
  • [ ] 当没有活跃对话时使用 GroupActivitySharingController
  • [ ] 会话失效时处理清理,包括 messenger、journal 和任务

参考资料