mcpField
S
shareplay-activities

shareplay-activities

github.com/dpearson2699開発1.2k3.6k 回インストール13 週間前に更新
リポジトリ

shareplay-activities について

shareplay-activitiesは、GroupActivitiesとSharePlayでリアルタイム共有体験を構築するAppleアプリ開発者向けスキルです。セッション管理、参加者間の状態同期、メディア再生の連携、添付ファイルの転送を解説します。

主な機能

  1. 01GroupActivityの定義と開始
  2. 02SharePlayセッションライフサイクルの管理
  3. 03メッセージによる参加者状態の同期
  4. 04AVPlaybackCoordinatorによる再生連携
  5. 05GroupSessionJournalによる添付ファイル転送

活用シーン

  1. 01FaceTimeでの共同視聴体験の構築
  2. 02共同描画や編集状態の同期
  3. 03途中参加者への現在状態の共有
  4. 04セッションでの非同期添付ファイル共有

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill shareplay-activities

インストール後、Claude Code で/shareplay-activitiesを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/shareplay-patterns.md

詳細

GroupActivities / SharePlay

GroupActivities フレームワークを使って、リアルタイムの共有体験を構築できます。SharePlay は FaceTime、Messages、AirDrop、および近くの visionOS 共有を通じて人々をつなぎ、メディア再生、アプリの状態、またはカスタムデータを同期します。

目次

セットアップ

Capability

Xcode でアプリターゲットに Group Activities capability を追加します。Xcode が必要なエンタイトルメントを追加し、プロビジョニングプロファイルを更新します。

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

これはアプリターゲットにのみ設定してください。Group Activities はウィジェット、拡張機能、App Clips では利用できません。

利用資格の確認
import GroupActivities

let observer = GroupStateObserver()

// Check if a FaceTime call or Messages conversation is active
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 であり、アクティビティに保存するデータは Codable である必要があります。Transferable を追加するのは、SwiftUI の ShareLink、AirDrop 経由の SharePlay、または AppKit/UIKit の共有シートを使う場合に限ります。ペイロードは最小限に保ち、大きなデータの代わりに識別子や 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)

        // 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
    }
}
セッションの状態
状態説明
.waitingセッションは存在しますが、ローカルの参加者はまだ参加していません
.joinedローカルの参加者がセッションに参加しています
.invalidated(reason:)セッションが終了しました(詳細は理由を確認してください)
状態変化の処理
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)")
}
退出と終了
// Leave the session (other participants continue)
session?.leave()

// End the session for all participants
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)
}

// Send to specific participants
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)
        }
    }
}
配信モード
// 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
)

選択やターンなど、状態を変更するアクションには .reliable を使います。カーソル位置、描画ストローク、リアクションなど、高頻度で一時的なデータには .unreliable を使います。

協調メディア再生

ビデオやオーディオには、AVPlayer と AVPlaybackCoordinator を使います。

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

接続すると、AVFoundation が再生/一時停止、シーク、レート、再生速度、時刻を同期します。後から参加する人向けのスナップショットを含め、AVPlayer の再生制御フィールドをメッセンジャーメッセージやスナップショットに含めないでください。再生以外の状態には、カスタムメッセージのみを使います。

アプリから 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:
        // 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
    }
}

アクティブな会話がない場合(つまり、isEligibleForGroupSession が false の場合)は、 GroupActivitySharingController を使用して、まずユーザーに連絡先を選択してもらいます。

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

カスタムコントロールには shareplay SF シンボルを使用します。GroupActivityMetadata は、簡潔なタイトル、サブタイトル、画像、エントリーポイントに沿ったタイプを含む、 検出用の紹介文として扱います。隣接する領域の役割を侵さないようにします。認証、マッチメイキング、 リーダーボード、実績、音声/チャットは GameKit が担当します。座席、ボード設備、 空間配置、ターン、ルール、権威あるテーブルトップの状態は TabletopKit が担当します。 再生 UI は AVKit が担当します。SharePlay は招待、ライフサイクル、参加者、 連携の引き継ぎを担当します。SwiftUI の ShareLink、AirDrop、 直接アクティベーションのパターンについては、references/shareplay-patterns.md を参照してください。

GroupSessionJournal:ファイル転送

サイズが大きく、時間的制約のない添付ファイルには、GroupSessionMessenger の代わりに GroupSessionJournal を使用します。ジャーナル項目は Transferable に準拠する必要があり、 後から参加するユーザーも利用でき、上限は 100 MB です。iOS/iPadOS/tvOS 17 以降、macOS 14 以降、または visionOS 1 以降が必要です。サイズが大きい、または保護されたアセットについては、 ポインターまたはマニフェストを共有し、サーバーストレージまたはアプリ管理のファイル転送を使用してください。

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

よくある間違い

してはいけないこと:session.join() の呼び出しを忘れる

保存されたセッション、メッセンジャー、オブザーバーを設定してから、join() を呼び出します。 Session Lifecycle の長期間存続するマネージャーの例に、必要な順序が示されています。

してはいけないこと:セッションを退出または終了し忘れる
// 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
}
してはいけないこと:すべての参加者が同じ状態だと想定する
// 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
}
してはいけないこと:大容量/保護されたアセットに SharePlay の転送機能を使う
// 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
してはいけないこと:メディア再生について重複したメッセージを送信する
// 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
してはいけないこと:再作成されるビューでセッションを監視する

再作成可能なビューではなく、長期間存続するマネージャーで sessions() リスナーを管理します。 上記のマネージャーのライフサイクルを使用し、無効化時には子タスクをキャンセルしてください。

レビューチェックリスト

  • Group Activities 機能をアプリのターゲットにのみ追加している
  • GroupActivity 構造体が Codable に準拠し、意味のあるメタデータを持っている
  • ShareLink、AirDrop、または共有シートを使用する場合、Transferable 準拠を追加している
  • 長期間存続するオブジェクトで sessions() を監視している(SwiftUI ビューの本体ではない)
  • セッションを受信して設定した後に session.join() を呼び出している
  • ユーザーが画面遷移するか閉じたときに session.leave() を呼び出している
  • GroupSessionMessenger のメッセージを適切な deliveryMode で 256 KB 未満に保っている
  • 後から参加するユーザーが接続時に現在の状態を受け取る
  • ライフサイクルの変更を検出するために $state と $activeParticipants のパブリッシャーを監視している
  • 時間的制約のない Transferable 添付ファイルに GroupSessionJournal を使用している
  • メディア同期には AVPlaybackCoordinator を使用している(手動のメッセージではない)
  • SharePlay UI を表示する前に GroupStateObserver.isEligibleForGroupSession を確認している
  • アクティブな会話がない場合に GroupActivitySharingController を使用している
  • セッションの無効化時に、メッセンジャー、ジャーナル、タスクをクリーンアップしている

参考資料

コメント