使用 GroupActivities 和 SharePlay 打造共享即時體驗。適用於實作共享媒體播放、協作應用功能、同步遊戲狀態,或任何在 iOS、macOS、tvOS 或 visionOS 上的 FaceTime、訊息、AirDrop 或鄰近 visionOS 群組活動。
GroupActivities / SharePlay
使用 GroupActivities 框架打造共享即時體驗。SharePlay
透過 FaceTime、訊息、AirDrop 以及鄰近 visionOS 分享連結使用者,
同步媒體播放、應用程式狀態或自訂資料。
目錄
- 設定
- 定義 GroupActivity
- Session 生命週期
- 傳送與接收訊息
- 協調媒體播放
- 從你的 App 啟動 SharePlay
- GroupSessionJournal:檔案傳輸
- 常見錯誤
- 審查清單
- 參考資料
設定
功能
在 Xcode 中為 App target 加入 Group Activities 功能。Xcode 會加入
所需的授權並更新 provisioning profile:
<key>com.apple.developer.group-session</key>
<true/>
僅為 App target 設定此功能。Group Activities 不適用於
widget、extension 或 App Clip。
檢查資格
import GroupActivities
let observer = GroupStateObserver()
// 檢查是否有進行中的 FaceTime 通話或訊息對話
if observer.isEligibleForGroupSession {
showSharePlayButton()
}
以響應式方式觀察變化:
for await isEligible in observer.$isEligibleForGroupSession.values {
showSharePlayButton(isEligible)
}
定義 GroupActivity
遵循 GroupActivity 協定並提供 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
}
}
活動類型
| 類型 | 使用情境 |
|---|---|
.generic |
自訂活動的預設值 |
.watchTogether |
影片播放 |
.listenTogether |
音訊播放 |
.createTogether |
協作創作(繪圖、編輯) |
.exploreTogether |
共享瀏覽、規劃或探索 |
.learnTogether |
共享學習或研讀 |
.readTogether |
共享閱讀 |
.shopTogether |
共享購物 |
.workoutTogether |
共享健身 |
GroupActivity 是 Codable;儲存的活動資料必須可編碼。僅在
使用 SwiftUI ShareLink、透過 AirDrop 的 SharePlay 或
AppKit/UIKit 分享選單時才加入 Transferable。保持 payload 最小化:使用識別碼或 URL
而非大型資料。
Session 生命週期
監聽 Session
建立一個長期存在的 task 來接收其他參與者啟動活動時的 session:
@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)
// 觀察 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
session.join()
}
private func cleanUp() {
sessionTasks.forEach { $0.cancel() }
sessionTasks.removeAll()
session = nil
messenger = nil
}
}
Session 狀態
| 狀態 | 說明 |
|---|---|
.waiting |
Session 存在但本地參與者尚未加入 |
.joined |
本地參與者已活躍於 session 中 |
.invalidated(reason:) |
Session 已結束(檢查 reason 取得詳細資訊) |
處理狀態變化
private func handleState(_ state: GroupSession<WatchTogetherActivity>.State) {
switch state {
case .waiting:
print("等待加入")
case .joined:
print("已加入 session")
loadActivity(session?.activity)
case .invalidated(let reason):
print("Session 已結束:\(reason)")
cleanUp()
@unknown default:
break
}
}
private func handleParticipants(_ participants: Set<Participant>) {
print("活躍參與者:\(participants.count)")
}
離開與結束
// 離開 session(其他參與者繼續)
session?.leave()
// 為所有參與者結束 session
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。
協調媒體播放
對於影片/音訊,使用 AVPlaybackCoordinator 搭配 AVPlayer:
import AVFoundation
import GroupActivities
func configurePlayback(
session: GroupSession<WatchTogetherActivity>,
player: AVPlayer
) {
// 將播放器的協調器連接到 session
let coordinator = player.playbackCoordinator
coordinator.coordinateWithSession(session)
}
連接後,AVFoundation 會同步播放/暫停、搜尋、播放速率、播放速度
和時間。請勿將 AVPlayer 的傳輸控制欄位放入 messenger 訊息或快照中,
包含後加入者的快照;僅針對播放以外的狀態使用自訂訊息。
從你的 App 啟動 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 Symbol。將 GroupActivityMetadata 視為
探索文案:簡潔的標題、副標題、圖片和類型,與進入點一致。
保持兄弟領域的區隔:GameKit 擁有認證、配對、排行榜、成就和語音/聊天;
TabletopKit 擁有座位、棋盤設備、空間擺放、回合、規則和權威的桌遊狀態;
AVKit 擁有播放 UI。SharePlay 擁有邀請、生命週期、參與者和協調交接。
參見 references/shareplay-patterns.md 了解 SwiftUI ShareLink、AirDrop 和直接啟動模式。
GroupSessionJournal:檔案傳輸
對於較大、非時間敏感的附件,使用 GroupSessionJournal 而非
GroupSessionMessenger。Journal 項目必須遵循 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()
設定好儲存的 session、messenger 和觀察者後,再呼叫 join()。
Session 生命週期中的典型長期管理員顯示了必要的順序。
不要:忘記離開或結束 session
// 錯誤——使用者離開頁面後 session 仍然存活
func viewDidDisappear() {
// 什麼都不做——session 洩漏
}
// 正確——當檢視被關閉時離開
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() // 自動同步給所有參與者
不要:在會被重建的檢視中觀察 session
將 sessions() 監聽器放在長期存在的管理員中,而不是可重建的檢視。
使用上述的管理員生命週期,並在失效時取消其子 task。
審查清單
- [ ] 僅為 App target 加入 Group Activities 功能
- [ ]
GroupActivity結構體是Codable且包含有意義的 metadata - [ ] 使用
ShareLink、AirDrop 或分享選單時加入Transferable遵循 - [ ]
sessions()在長期存在的物件中觀察(而非 SwiftUI 檢視 body) - [ ] 接收並設定 session 後呼叫
session.join() - [ ] 使用者離開或關閉時呼叫
session.leave() - [ ]
GroupSessionMessenger訊息保持在 256 KB 以下,並使用適當的deliveryMode - [ ] 後加入的參與者在連線時收到當前狀態
- [ ] 觀察
$state和$activeParticipants發布者以處理生命週期變化 - [ ] 對於非時間敏感的
Transferable附件使用GroupSessionJournal - [ ] 使用
AVPlaybackCoordinator進行媒體同步(而非手動訊息) - [ ] 在顯示 SharePlay UI 前檢查
GroupStateObserver.isEligibleForGroupSession - [ ] 當沒有進行中的對話時使用
GroupActivitySharingController - [ ] 處理 session 失效,清理 messenger、journal 和 task
參考資料
- 擴展模式(SwiftUI 分享、協作畫布、空間 Persona):references/shareplay-patterns.md
- Configuring Group Activities
- GroupActivities framework
- GroupActivity protocol
- GroupSession
- GroupSessionMessenger
- GroupSessionJournal
- GroupStateObserver
- GroupActivitySharingController
- Defining your app's SharePlay activities
- Presenting SharePlay activities from your app's UI
- Synchronizing data during a SharePlay activity
- Supporting coordinated media playback
- SharePlay HIG




