shareplay-activities

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GroupActivities / SharePlay

GroupActivities / SharePlay

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.2 / iOS 26+.
使用GroupActivities框架构建共享实时体验。SharePlay通过FaceTime或iMessage连接用户,同步媒体播放、应用状态或自定义数据。目标环境为Swift 6.2 / iOS 26+。

Contents

目录

Setup

配置步骤

Entitlements

权限配置

Add the Group Activities entitlement to your app:
xml
<key>com.apple.developer.group-session</key>
<true/>
为你的应用添加Group Activities权限:
xml
<key>com.apple.developer.group-session</key>
<true/>

Info.plist

Info.plist配置

For apps that start SharePlay without a FaceTime call (iOS 17+), add:
xml
<key>NSSupportsGroupActivities</key>
<true/>
对于无需FaceTime通话即可启动SharePlay的应用(iOS 17+),添加:
xml
<key>NSSupportsGroupActivities</key>
<true/>

Checking Eligibility

检查可用性

swift
import GroupActivities

let observer = GroupStateObserver()

// Check if a FaceTime call or iMessage group is active
if observer.isEligibleForGroupSession {
    showSharePlayButton()
}
Observe changes reactively:
swift
for await isEligible in observer.$isEligibleForGroupSession.values {
    showSharePlayButton(isEligible)
}
swift
import GroupActivities

let observer = GroupStateObserver()

// 检查是否存在活跃的FaceTime通话或iMessage群组
if observer.isEligibleForGroupSession {
    showSharePlayButton()
}
响应式监听状态变化:
swift
for await isEligible in observer.$isEligibleForGroupSession.values {
    showSharePlayButton(isEligible)
}

Defining a GroupActivity

定义GroupActivity

Conform to
GroupActivity
and provide metadata:
swift
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
    }
}
遵循
GroupActivity
协议并提供元数据:
swift
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
    }
}

Activity Types

活动类型

TypeUse 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.
类型适用场景
.generic
自定义活动默认类型
.watchTogether
视频播放
.listenTogether
音频播放
.createTogether
协作创作(绘图、编辑)
.workoutTogether
共享健身会话
活动结构体必须遵循
Codable
协议,以便系统在设备间传输数据。

Session Lifecycle

会话生命周期

Listening for Sessions

监听会话

Set up a long-lived task to receive sessions when another participant starts the activity:
swift
@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()
    }
}
设置长生命周期任务,接收其他参与者启动的活动会话:
swift
@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)

        // 监听会话状态变化
        Task {
            for await state in session.$state.values {
                handleState(state)
            }
        }

        // 监听参与者变化
        Task {
            for await participants in session.$activeParticipants.values {
                handleParticipants(participants)
            }
        }

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

Session States

会话状态

StateDescription
.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)
状态描述
.waiting
会话已存在,但本地参与者尚未加入
.joined
本地参与者已活跃加入会话
.invalidated(reason:)
会话已结束(查看reason获取详情)

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

Leaving and Ending

离开与结束会话

swift
// Leave the session (other participants continue)
session?.leave()

// End the session for all participants
session?.end()
swift
// 离开会话(其他参与者可继续)
session?.leave()

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

Sending and Receiving Messages

消息收发

Use
GroupSessionMessenger
to sync app state between participants.
使用
GroupSessionMessenger
在参与者之间同步应用状态。

Defining Messages

定义消息

Messages must be
Codable
:
swift
struct SyncMessage: Codable {
    let action: String
    let timestamp: Date
    let data: [String: String]
}
消息必须遵循
Codable
协议:
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 participants
try await messenger.send(message, to: .only(participant))
swift
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))

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)
        }
    }
}
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) -- 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).
swift
// 可靠模式(默认)-- 保证传输且有序
let reliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .reliable
)

// 不可靠模式 -- 速度更快,无保障(适合频繁的位置更新)
let unreliableMessenger = GroupSessionMessenger(
    session: session,
    deliveryMode: .unreliable
)
状态变更操作(播放/暂停、选择)使用
.reliable
模式。高频临时数据(光标位置、绘图笔触)使用
.unreliable
模式。

Coordinated Media Playback

协调媒体播放

For video/audio, use
AVPlaybackCoordinator
with
AVPlayer
:
swift
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.
对于视频/音频,将
AVPlaybackCoordinator
AVPlayer
配合使用:
swift
import AVFoundation
import GroupActivities

func configurePlayback(
    session: GroupSession<WatchTogetherActivity>,
    player: AVPlayer
) {
    // 将播放器的协调器与会话关联
    let coordinator = player.playbackCoordinator
    coordinator.coordinateWithSession(session)
}
关联后,任何参与者播放器上的播放/暂停/跳转操作都会自动同步到所有其他参与者,无需手动传递消息实现播放控制。

Handling Playback Events

处理播放事件

swift
// Notify participants about playback events
let event = GroupSessionEvent(
    originator: session.localParticipant,
    action: .play,
    url: nil
)
session.showNotice(event)
swift
// 通知参与者播放事件
let event = GroupSessionEvent(
    originator: session.localParticipant,
    action: .play,
    url: nil
)
session.showNotice(event)

Starting SharePlay from Your App

从应用内启动SharePlay

Using GroupActivitySharingController (UIKit)

使用GroupActivitySharingController(UIKit)

swift
import GroupActivities
import UIKit

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

    switch await activity.prepareForActivation() {
    case .activationPreferred:
        // Present the sharing controller
        let controller = try GroupActivitySharingController(activity)
        present(controller, animated: true)

    case .activationDisabled:
        // SharePlay is disabled or unavailable
        print("SharePlay not available")

    case .cancelled:
        break

    @unknown default:
        break
    }
}
For
ShareLink
(SwiftUI) and direct
activity.activate()
patterns, see
references/shareplay-patterns.md
.
swift
import GroupActivities
import UIKit

func startSharePlay() async throws {
    let activity = WatchTogetherActivity(
        movieID: "123",
        movieTitle: "精彩电影"
    )

    switch await activity.prepareForActivation() {
    case .activationPreferred:
        // 展示共享控制器
        let controller = try GroupActivitySharingController(activity)
        present(controller, animated: true)

    case .activationDisabled:
        // SharePlay不可用或已禁用
        print("SharePlay不可用")

    case .cancelled:
        break

    @unknown default:
        break
    }
}
关于
ShareLink
(SwiftUI)和直接调用
activity.activate()
的模式,请查看
references/shareplay-patterns.md

GroupSessionJournal: File Transfer

GroupSessionJournal:文件传输

For large data (images, files), use
GroupSessionJournal
instead of
GroupSessionMessenger
(which has a size limit):
swift
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)
        }
    }
}
对于大型数据(图片、文件),请使用
GroupSessionJournal
而非
GroupSessionMessenger
(后者有单条消息大小限制):
swift
import GroupActivities

let journal = GroupSessionJournal(session: session)

// 上传文件
let attachment = try await journal.add(imageData)

// 监听传入的附件
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()

错误做法:忘记调用session.join()

swift
// 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()
}
swift
// 错误 -- 接收会话但从未加入
for await session in MyActivity.sessions() {
    self.session = session
    // 会话将一直处于.waiting状态
}

// 正确 -- 配置后加入会话
for await session in MyActivity.sessions() {
    self.session = session
    self.messenger = GroupSessionMessenger(session: session)
    session.join()
}

DON'T: Forget to leave or end sessions

错误做法:忘记离开或结束会话

swift
// 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
}
swift
// 错误 -- 用户离开后会话仍保持活跃
func viewDidDisappear() {
    // 无操作 -- 会话出现泄漏
}

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

DON'T: Assume all participants have the same state

错误做法:假设所有参与者状态一致

swift
// 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
}
swift
// 错误 -- 广播状态但未处理迟加入的参与者
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
}

DON'T: Use GroupSessionMessenger for large data

错误做法:使用GroupSessionMessenger传输大型数据

swift
// 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)
swift
// 错误 -- messenger有单条消息大小限制
let largeImage = try Data(contentsOf: imageURL)  // 5 MB
try await messenger.send(largeImage, to: .all)    // 可能失败

// 正确 -- 使用GroupSessionJournal传输文件
let journal = GroupSessionJournal(session: session)
try await journal.add(largeImage)

DON'T: Send redundant messages for media playback

错误做法:手动同步媒体播放消息

swift
// 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
swift
// 错误 -- 使用AVPlayer时手动同步播放/暂停
func play() {
    player.play()
    try await messenger.send(PlayMessage(), to: .all)
}

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

DON'T: Observe sessions in a view that gets recreated

错误做法:在会重建的视图中监听会话

swift
// 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)
            }
        }
    }
}
swift
// 错误 -- 每次视图出现时都会创建新的监听器
struct MyView: View {
    var body: some View {
        Text("Hello")
            .task {
                for await session in MyActivity.sessions() { }
            }
    }
}

// 正确 -- 在长生命周期的管理器中监听会话
@Observable
final class ActivityManager {
    init() {
        Task {
            for await session in MyActivity.sessions() {
                configureSession(session)
            }
        }
    }
}

Review Checklist

审核检查清单

  • Group Activities entitlement (
    com.apple.developer.group-session
    ) added
  • GroupActivity
    struct is
    Codable
    with meaningful metadata
  • 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
    created with appropriate
    deliveryMode
  • Late-joining participants receive current state on connection
  • $state
    and
    $activeParticipants
    publishers observed for lifecycle changes
  • GroupSessionJournal
    used for large file transfers instead of messenger
  • AVPlaybackCoordinator
    used for media sync (not manual messages)
  • GroupStateObserver.isEligibleForGroupSession
    checked before showing SharePlay UI
  • prepareForActivation()
    called before presenting sharing controller
  • Session invalidation handled with cleanup of messenger, journal, and tasks
  • 已添加Group Activities权限(
    com.apple.developer.group-session
  • GroupActivity
    结构体遵循
    Codable
    协议并包含有意义的元数据
  • 在长生命周期对象中监听
    sessions()
    (而非SwiftUI视图体)
  • 接收并配置会话后调用了
    session.join()
  • 用户离开或关闭界面时调用了
    session.leave()
  • GroupSessionMessenger
    配置了合适的
    deliveryMode
  • 迟加入的参与者在连接时能收到当前状态
  • 监听了
    $state
    $activeParticipants
    发布者以处理生命周期变化
  • 大型文件传输使用
    GroupSessionJournal
    而非messenger
  • 媒体同步使用
    AVPlaybackCoordinator
    (而非手动消息)
  • 展示SharePlay UI前检查了
    GroupStateObserver.isEligibleForGroupSession
  • 展示共享控制器前调用了
    prepareForActivation()
  • 会话失效时清理了messenger、journal和任务

References

参考资料