WWDC Quick Look 💓 By SwiftGGTeam
Build custom experiences with Group Activities

Build custom experiences with Group Activities

元の動画を見る

ハイライト

Apple はカスタムアクティビティを提供しており、GroupSessionMessenger、開発者は、FaceTime で Codable メッセージを同期し、スケッチパッドや共同編集などの非メディア アプリをリアルタイムの SharePlay エクスペリエンスに変えることができます。

主要内容

スケッチパッド アプリから始める

スケッチパッド アプリはシンプルだったかもしれません。画面の中央にはキャンバスがあり、各人に色があり、指が動くと現在のストロークにポイントが追加されます。

問題はFaceTime通話中に発生します。 3 人が同じキャンバス上に描画したい場合、各デバイスは他のデバイスのストロークを受け取ります。新しく参加する人も、すでに描かれているコンテンツを入手する必要があります。誰かがキャンバスをクリアすると、他のデバイスも同じ新しい状態に切り替える必要があります。

メディア アプリは再生同期に依存できます。アートボードにはプレーヤーもタイムラインもありません。開発者は「どのデータを共有するか」を自分で定義し、そのデータをセッションの他の参加者に送信する必要があります。

Apple は、このセッションで完全なプロセスをデモンストレーションするために DrawTogether を使用しました。まず、GroupActivity、カスタム アクティビティとしてマークします。それから使用しますGroupSessionMessenger参加者間で構造化されたメッセージを送信します。最後に、遅延参加者、アクティビティの切り替え、エントリ ボタンなどのエッジ ケースを処理します。

カスタムアクティビティは安定した情報のみを保存します

GroupActivity描かれているのは共有された経験そのものです。スピーチでは、アクティビティには体験を通じて変わらない情報が含まれるべきであると強調しました。

DrawTogether の場合、安定したメッセージは「みんなが一緒に絵を描いています」です。現在どの線が描かれているか、各線上にどのような点があるかはユーザーの操作によって変化するため、メッセージを通じて同期する必要があります。

したがって、カスタム アクティビティを構成するときは、メタデータのタイトルを設定し、metadata.typeに設定.generic。このステップにより、メディア キャンペーンからカスタム キャンペーンに変わります。

メッセージは共有キャンバスの中核です

キャンバスの同期では、画像全体を転送する必要はありません。ユーザーが新しいドットを描くたびに、アプリはメッセージを送信します。メッセージにはストローク ID、色、座標点が含まれます。

GroupSessionMessengerセッションの参加者に Codable メッセージを送信する責任を負い、指定されたタイプのメッセージを受信するための非同期シーケンスも提供します。このスピーチは、これが小さなペイロードに適しており、ファイル、画像、ビデオの転送には適していないことを明確に思い出させました。大量のメッセージを高速ループで送信すると、次のような問題が発生する可能性があります。send間違った投げ方。

経験はエッジケースにも対処する必要がある

共有された経験は、遅れて参加する人たちと出会うでしょう。新しいデバイスが追加されると、以前のキャンバス状態は残りません。直接描画を開始すると、他のデバイスと絵が一致しなくなります。

演説で示された計画は監視することであるGroupSession.activeParticipants。参加者のセットが変更されると、既存のデバイスは新しい参加者をカウントし、キャッチアップ メッセージとして現在のキャンバスを送信します。

新しいキャンバスに切り替える場合、Lecture では新しいグループ セッションを作成することをお勧めします。このアプローチは、古い状態と新しい状態の間に明確な境界を追加し、ユーザーが大きな変更を開始したことをシステムに知らせます。

詳細

カスタム グループ アクティビティを構成する (03:50)

Appleが提供する入り口は、GroupActivityプロトコル。 DrawTogether は同じ名前の構造体を定義し、実装しますmetadata、タイプを次のように設定します。.generic

import GroupActivities

struct DrawTogether: GroupActivity {

    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.title = NSLocalizedString("Draw Together",
                                           comment: "Title of group activity")
        metadata.type = .generic
        return metadata
    }

}

キーポイント:

  • import GroupActivitiesSharePlay関連のフレームワークを紹介します。
  • struct DrawTogether: GroupActivityシステムで共有できるアクティビティのタイプを定義します。
  • var metadata: GroupActivityMetadataシステム UI に必要な情報を表示するアクティビティを返します。
  • GroupActivityMetadata()メタデータオブジェクトを作成します。
  • metadata.titleFaceTime 共有インターフェイスで使用されるイベントのタイトルを設定します。
  • metadata.type = .genericアクティビティをカスタム エクスペリエンスとしてマークします。
  • return metadata設定はシステムに任せます。

プレゼンテーションでは、アクティベーションのプロセスも実演しました。ユーザーがコントロール バー ボタンをクリックすると、アプリはDrawTogetherインスタンスと呼び出しactivate()。プロジェクトは、セッションに参加する前にグループ アクティビティの権利を追加する必要もあります。

import GroupActivities
import SwiftUI

struct ShareCanvasButton: View {
    var body: some View {
        Button {
            Task {
                _ = try? await DrawTogether().activate()
            }
        } label: {
            Image(systemName: "shareplay")
        }
    }
}

キーポイント:

  • Buttonこれは、DrawTogether デモの ControlBar に配置されるエントリです。
  • Task非同期 API を呼び出すためのボタン アクションを有効にします。
  • DrawTogether()定義したカスタム アクティビティを作成します。
  • activate()このグループ アクティビティを開始するようにシステムに要求します。
  • try? awaitアクティブ化が非同期で完了し、失敗する可能性があることを示します。

グループセッションを受信して​​参加します (08:03)

アクティビティがアクティブ化された後、アプリは受信する必要がありますGroupSession。スピーチではこの一歩を踏み出しましたContentView非同期タスクで、セッションを Canvas オブジェクト設定に渡し、最後に呼び出しますjoin()

import GroupActivities
import SwiftUI

struct ContentView: View {
    @StateObject private var canvas = Canvas()

    var body: some View {
        CanvasView(canvas: canvas)
            .task {
                for await groupSession in DrawTogether.sessions() {
                    canvas.configureGroupSession(groupSession)
                    groupSession.join()
                }
            }
    }
}

キーポイント:

  • .taskSwiftUI ビューが表示されたら、非同期リスナーを開始します。
  • DrawTogether.sessions()はいGroupActivity提供されたセッションの非同期シーケンス。
  • for await継続的に新作を受け取るGroupSession
  • canvas.configureGroupSession(groupSession)セッションをアプリ独自のモデル レイヤーに保存します。
  • groupSession.join()現在のデバイスを共有エクスペリエンスに参加させます。

レクチャー内のキャンバスは、セッションを構成するときに、まずローカル キャンバスをリセットし、それから保存します。groupSession。こうすることで、新しいセッションが古いキャンバスのローカル状態と混ざることがなくなります。

GroupSessionMessenger とストロークを同期する (10:06)

GroupSessionMessengerこれはこのスピーチの中核となる API です。生データまたは構造化メッセージを送信できます。 DrawTogether は、ストローク ポイントを Codable タイプとして自然に表現できるため、構造化メッセージを選択します。

import GroupActivities
import SwiftUI

let messenger = GroupSessionMessenger(session: groupSession)

// 1. Define
struct UpsertStrokeMessage: Codable {
    let id: UUID
    let color: Color
    let point: CGPoint
}

// 2. Receive
for await (message, context) in messenger.messages(of: UpsertStrokeMessage.self) {
    // Handle message
}

// 3. Send
do {
    try await messenger.send(UpsertStrokeMessage(id: stroke.id, color: .red, point: point))
} catch {
    // Handle error
}

キーポイント:

  • GroupSessionMessenger(session: groupSession)現在のグループセッションをバインドします。
  • UpsertStrokeMessageキャンバス同期プロトコルのメッセージです。
  • id同じストロークを識別し、受信側はそれに応じてポイントを追加できます。
  • colorこのストロークの色を保存します。
  • point今回は新しく追加した座標点を保存します。
  • Codableメッセンジャーにシリアル化と逆シリアル化を自動的に処理させます。
  • messenger.messages(of:)要素にメッセージとコンテキストが含まれる非同期シーケンスを返します。
  • context送信者などのメッセージ コンテキストが含まれます。
  • messenger.send非同期スローメソッドであるため、アプリは失敗を処理する必要があります。

受信側の処理ロジックも非常に単純です。ローカルに同じ ID を持つストロークがすでに存在する場合は、それにポイントを追加します。そうでない場合は、新しいストロークを作成し、キャンバス配列に追加します。

import SwiftUI

struct Stroke: Identifiable {
    let id: UUID
    let color: Color
    var points: [CGPoint]
}

func handle(_ message: UpsertStrokeMessage, strokes: inout [Stroke]) {
    if let index = strokes.firstIndex(where: { $0.id == message.id }) {
        strokes[index].points.append(message.point)
    } else {
        let stroke = Stroke(id: message.id,
                            color: message.color,
                            points: [message.point])
        strokes.append(stroke)
    }
}

キーポイント:

  • Stroke実際にキャンバスに保存されたストロークモデルです。
  • id対応するUpsertStrokeMessage.id、同じストロークを見つけるために使用されます。
  • pointsこのストロークが受け取ったすべてのポイントを保存します。
  • firstIndex(where:)このストロークがローカルに既に存在するかどうかを確認してください。
  • 見つかった場合のみ追加しますmessage.point
  • 見つからない場合は新規作成Stroke、その後、参加しますstrokes

このプレゼンテーションでは 3 つの制限が追加されます。 Messenger は、信頼性の高い FIFO 順序のメッセージ配信を提供します。ニュースが大きすぎると、sendエラーがスローされます。大量のメッセージを短期間に送信する場合、send間違って投げられることもあります。アプリケーション プロトコルでは、古いアプリと新しいアプリが相互運用できるように、バージョン フィールドも考慮する必要があります。

遅れて参加した人に現在のキャンバスを取得させる (16:07)

遅れて参加した人がグループ セッションに参加した後、キャンバスは最初は空です。スピーチの扱いは定義ですCanvasMessage、現在のストロークをすべて結合します。pointCountヒューリスティック カウントは新しいデバイスに一緒に送信されます。

import SwiftUI

struct CanvasMessage: Codable {
    let strokes: [Stroke]
    let pointCount: Int
}

func handle(_ message: CanvasMessage,
            strokes: inout [Stroke],
            currentPointCount: Int) {
    guard message.pointCount > currentPointCount else {
        return
    }

    strokes = message.strokes
}

キーポイント:

  • CanvasMessageキャッチアップデータを表します。
  • strokes現在のキャンバスのストローク全体を保持します。
  • pointCountメッセージが古いか新しいかを判断するためのヒューリスティック インジケーターとして使用されます。
  • guard message.pointCount > currentPointCount古いメッセージが新しいキャンバスを覆うことは避けてください。
  • strokes = message.strokesローカル キャンバスを新しいキャッチアップ データに置き換えます。

キャッチアップ データを送信するトリガー ポイントは次のとおりです。activeParticipants。既存のデバイスは、参加者セットの変更を監視し、新しい参加者を計算し、現在のキャンバスをこれらの新しい参加者にのみ送信します。

import GroupActivities

func sendCatchUpMessage(
    to newParticipants: Set<GroupSession<DrawTogether>.Participant>,
    messenger: GroupSessionMessenger,
    strokes: [Stroke],
    pointCount: Int
) async {
    let message = CanvasMessage(strokes: strokes, pointCount: pointCount)

    do {
        try await messenger.send(message, to: .only(newParticipants))
    } catch {
        // Handle error
    }
}

キーポイント:

  • newParticipantsからですactiveParticipants新しい参加者は変更で計算されます。
  • CanvasMessage(strokes:pointCount:)現在のキャンバスのスナップショットを作成します。
  • messenger.send構造化されたメッセージの送信を担当します。
  • .only(newParticipants)遅れて参加する参加者へのメッセージを制限します。
  • catchメッセージ サイズや送信スロットリングなどの送信失敗を処理します。

適切な場合にのみ SharePlay ボタンを表示します (22:58)

共有ボタンは常に表示されるべきではありません。ユーザーがグループ セッションを開く環境にない場合、ボタンは誤解を招く可能性があります。

GroupStateObserver資格情報を提供します。 Speech はこれを使用して、デバイスがグループ セッションを開始できるかどうかを監視し、ボタンを動的に表示または非表示にします。

import GroupActivities
import SwiftUI

struct ControlBar: View {
    @StateObject private var groupStateObserver = GroupStateObserver()
    let isInGroupSession: Bool

    var body: some View {
        HStack {
            if groupStateObserver.isEligibleForGroupSession && !isInGroupSession {
                ShareCanvasButton()
            }

            Button("Reset") {
                // Reset canvas
            }
        }
    }
}

キーポイント:

  • GroupStateObserver現在のデバイスがグループ セッションを開くのに適しているかどうかを確認します。
  • isEligibleForGroupSessiontrue の場合、ユーザーはエクスペリエンスの共有を開始できます。
  • !isInGroupSessionユーザーがすでにセッションに参加しているときに、起動ボタンを繰り返し表示しないようにします。
  • ShareCanvasButton()以前の呼び出しを再利用するactivate()入り口。
  • Resetキャンバスをクリアして新しいセッションに切り替える、レクチャー内のコントロールに対応します。

重要ポイント

1. コラボレーション ホワイトボード

  • やるべきこと: ペン、消しゴム、透明なキャンバスをサポートする共同描画ホワイトボードを FaceTime で作成します。
  • 実行する価値がある理由:GroupSessionMessenger各ストロークポイントを同期させることができ、遅れて結合する人が通過することができますCanvasMessage現在のキャンバスを取得します。
  • 開始方法: 最初に定義しますWhiteboardActivity: GroupActivity、設定metadata.type = .generic;再定義UpsertStrokeMessage: Codable、使用messenger.messages(of:)ストロークを受け取るには、次を使用します。messenger.sendローカル入力を送信します。

2. リモート ペア コード アノテーション

  • 内容: 2 人が FaceTime で同じコードを表示し、カーソル、選択内容、コメントを同期します。
  • 実行する価値がある理由: メッセージのペイロードが小さく、送信に適していますfileIDrangecommentIDこのような構造化データ。
  • 開始方法: 現在のファイル識別子を入力します。GroupActivity安定した情報でカーソルの移動と注釈の作成をコーディング可能なメッセージとして定義します。使用context誰が操作を送信したかを区別します。

3. 共通の買い物リスト

  • 作業内容: 通話中に複数人で買い物リストを整理し、リアルタイムで商品の追加、確認、削除を行います。
  • 実行する価値がある理由: それぞれの変更は小さなメッセージとして表現でき、メッセンジャーの小さなペイロードの位置付けと一致しています。
  • 開始方法: 定義UpsertItemMessageそしてDeleteItemMessage; 各メッセージにはアイテム ID とバージョン番号が含まれます。遅れて参加する人は、完全なインベントリ メッセージを通じて現在のステータスを把握します。

4. ボードゲームスコアラー

  • やるべきこと: すべてのプレーヤーのデバイスに同じラウンドとスコアを表示する通話中のボード ゲーム スコアラーを作成します。
  • 実行する価値がある理由: スコアの変更は低頻度の構造化データであり、使用に適しています。GroupSessionMessenger一貫性を保ちましょう。
  • 開始方法: を使用しますGroupActivityゲームを表します。使用ScoreUpdateMessageプレイヤー ID、スコア、ターン番号を送信します。聞くactiveParticipants現在のスコアボードを新しいプレーヤーに送信します。

5. レビューボードを共有する

  • やるべきこと: 複数の人が話しているときに、やるべきこと、質問、結論をまとめて整理し、カードの動きがリアルタイムで同期されます。
  • 実行する価値がある理由: カードの作成、移動、タイトルの変更は Codable メッセージに分割でき、システムは参加者間のメッセージ配信を担当します。
  • 開始方法: 定義CardMessageMoveCardMessageそしてBoardSnapshotMessage;使用pointCount同様のバージョンカウントにより、古いスナップショットが新しいボードを上書きするのを防ぎます。

関連セッション

コメント

GitHub Issues · utterances