【iOS 15】アプリにSharePlayを追加して、FaceTime通話中に参加者間でデータをやり取り(Group Activities)の完全なガイド

# はじめに

SharePlayはWWDC 2021でiOS 15に導入された新技術です。

この記事は、SharePlayを使ったデータ共有機能の実装についてご説明します。

手順に従って、SharePlayのセッションを開始し、FaceTime参加者とデータをやり取りするアプリを作ります。

#### プロセスを詳しく説明するため、図をいくつか作成して記事に追加しました。画像をクリックすると拡大できます。

## SharePlayについて

ユーザーの立場で言うと、このフレームワークを使った場合、FaceTimeの参加者がオンラインの活動を一緒にやる（例えば映画の鑑賞など）ことができるようになります。

開発者の視点で言うと、FaceTimeの参加者の間のデータ交換が可能になります。

そのため、一緒に映画を見る場合、FaceTime参加者間では再生時間のみが同期され、映画は各端末からストリーミング再生されることになります。

動画配信: Internet -> ユーザーの端末
再生時間: 他のFaceTimeの参加者 -> SharePlay -> ユーザーの端末

## SharePlayセッションのフロー図

セッションのホストの場合：

![スクリーンショット 2021-12-03 17.21.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/e62748c2-e35d-f8a3-84cf-7632061fb611.png)

既存のセッションに参加するの場合：

![スクリーンショット 2021-12-03 17.35.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/9b6965e9-bbfe-8b8a-6d03-82963a44897d.png)

# 実装

## エンタイトルメントの追加

まず、`Signing & Capabilities`タブで、`Group Activities`をエンタイトルメントファイルに追加します。

![スクリーンショット 2021-11-30 20.52.45.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/767dda37-eddb-92e4-1bf2-8c3a9fa48e7d.jpeg)

## この記事に関連するファイル

| File name | Purposes |
|---|---|
| DemoGroupActivityType.swift  | グループ活動の種類を定義する。 |
| MessageDataType.swift  | FaceTime参加者間で転送されるデータの種類を定義する。 |
| GroupActivityManager.swift  | 新しいグループセッションをホストする機能、グループセッションを設定する機能（データを受信するハンドラの設定、参加者リストの変更を処理）、データを送信する機能を提供。 |
| ContentsView.swift | これはSwiftUIビューで、そこではグループ・アクティビティーを開始するためのボタン、他のFaceTime参加者にランダムUUIDを送信するためのボタンが提供されるとともに、アクティブなグループ・セッションの監視が可能です。この例ではSwiftUIが使われていますが、UIKitを使用して同じ機能を提供することもできます。 |

## データタイプの定義

まず、転送したいデータのタイプを定義する必要があります。データタイプは `Codable` プロトコルに準拠する必要があります。

この例では、UUIDのみを転送します。

```swift
// MessageDataType.swift
struct DemoMessage: Codable {
    let id: UUID
}
```

## SharePlayのアクティビティの種類 `GroupActivity`

![スクリーンショット 2021-12-03 21.21.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/7e3114a4-4396-44e4-4fdc-d32b6fee8d0e.png)

あなたのアプリが提供するグループアクティビティのメタデータを提供する必要があります。このメタデータには、アクティビティの名前、種類、フォールバックURLが含まれます。

フォールバックURLは、アプリをダウンロードするためのリンクが含まれているウェブページに設定します。

これらの情報は、FaceTimeのチャットウィンドウに表示されます。

```swift
// DemoGroupActivityType.swift
import Foundation
import GroupActivities

struct DemoGroupActivityType: GroupActivity {
    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.title = "Demo Activity"
        metadata.type = .generic
        metadata.fallbackURL = URL(string: "https://example.com")
        return metadata
    }
}
```

このアクティビティタイプのオブジェクトはグループアクティビティをスタートするため、また現在進行中の全てのグループアクティビティのセッションをフェッチするためにも使用されます。

![スクリーンショット 2021-12-03 21.19.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/e587edde-f8b0-d294-873a-c2dced8dc1a3.png)

## セッション・マネージャーを定義

![スクリーンショット 2021-12-03 17.42.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/f0f6b906-5d4f-374f-d425-3790e3d3b640.png)

ここで、データを保存してグループアクティビティセッションを制御するためにセッションマネージャー設定を作成する必要があります。このセッションマネージャの使用法は以下の通りです。

1. グループ活動で共有したデータを保存する
2. グループ活動を開催する
3. 他の人にデータを送る
4. 受信したデータを処理する
5. 参加者リストの変更を処理する

```swift
// GroupActivityManager.swift
import Foundation
import Combine
import GroupActivities

@MainActor
class GroupActivityManager: ObservableObject {
    
    // SharePlay session
    @Published var groupSession: GroupSession<DemoGroupActivityType>?
    var messenger: GroupSessionMessenger?
    
    // Combine related
    var subscriptions = Set<AnyCancellable>()
    var tasks = Set<Task<Void, Never>>()
    
}
```

`groupSession`は、グループセッションの状態や参加者のリストを管理します。

`messenger`は､データの送受信に使用します。

`subscriptions`変数は、グループ・セッションの状態や参加者リストの変更を監視するのに役立ちます。

`tasks`は、データを受信するためのタスクを格納します。

## グループセッションの初期化ロジックを設定する

ユーザーがグループセッションに参加したり、グループセッションをホストしたりするたびに、セッションデータを設定する必要があります。

そのためには、`DemoGroupActivityType.sessions()`が変更されたときに通知するようシステムに依頼できます。ユーザーがグループセッションをホストしたり、グループセッションに参加したりしたことがわかるのです。

![スクリーンショット 2021-12-03 19.59.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/0d7d4f89-b402-3ef9-c66c-131de83a5ad4.png)

```swift
struct ContentView: View {
    
    @StateObject var manager = GroupActivityManager()
    
    var body: some View {
        
        VStack {
            Text("Hello world!")
        }
            .task {
                for await session in DemoGroupActivityType.sessions() {
                    manager.configureGroupSession(session)
                }
            }
        
    }
    
}
```

上記の例はSwiftUI用に書かれています。しかしながら、UIKitでも（セッションのリストの変更を監視するために）同じ待機ロジックを簡単に実装できるはずです。

#### `configureGroupSession`関数

グループセッションの設定段階では、FaceTime参加者間でデータを転送するための`GroupSessionMessenger`オブジェクトを設定する必要があります。また、受信データのハンドラも設定する必要があります。

`GroupActivityManager`クラスに新しい関数`configureGroupSession`を追加します。

![スクリーンショット 2021-12-03 21.38.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/8303ed9f-b554-e8fd-d006-9d442e55a911.png)

```swift
class GroupActivityManager: ObservableObject {
    
    /* ... Other Codes ... */
    
    func configureGroupSession(_ groupSession: GroupSession<DemoGroupActivityType>) {
        // セッションオブジェクトをローカル変数に格納し、（データの送受信に使用される）メッセンジャーオブジェクトを生成および格納します。
        self.groupSession = groupSession
        let messenger = GroupSessionMessenger(session: groupSession)
        self.messenger = messenger
        
        // セッション無効化の状態を処理
        groupSession.$state
            .sink { state in
                if case .invalidated = state {
                    self.groupSession = nil
                    self.reset()
                }
            }
            .store(in: &subscriptions)
        
        // 参加者リストの変更を処理
        groupSession.$activeParticipants
            .sink { activeParticipants in
                print(activeParticipants)
            }
            .store(in: &subscriptions)
        
        // タイプDemoMessageの入力データを処理
        let task = Task {
            for await (message, _) in messenger.messages(of: DemoMessage.self) {
                handle(message)
            }
        }
        tasks.insert(task)
        
        groupSession.join()
        
    }

    func handle(_ message: DemoMessage) {
        print("Received message: \(message)")
        latestUUID = message.id
    }

    /* ... Other Codes ... */
    
}
```

これで、データをグループアクティビティから受信すると、`handle(message)`関数が呼び出されます。

上の例では、タイプ`DemoMessage`のオブジェクトを転送しています。

## SharePlayセッションの開始

一般的なグループアクティビティのセッションでは、誰かがグループアクティビティ（例：映画を視聴する）を開始（ホスト）します。

既存のアクティビティがある状態でアクティビティの開始を選択した場合は、既存のアクティビティを置き換える必要があるかどうかを尋ねられます。

以下のコードを`GroupActivityManager`クラスに追加します。

```swift
func startSharing() {
    Task {
        do {
            _ = try await DemoGroupActivityType().activate()
        } catch {
            print("Failed to activate DrawTogether activity: \(error)")
        }
    }
}
```

ご注意：`activate()`関数を呼び出してグループセッションを開始すると、アプリはグループセッションの開始をリクエストします。

このようなリクエストが成功すると、アプリの`DemoGroupActivityType.sessions()`変数がシステムによって変更されます。

![スクリーンショット 2021-12-03 20.01.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/8ca79490-a204-a668-a88c-a7ae177598ff.png)

この変数は、新しく作成されたグループセッションのオブジェクトを取得するための方法です。ですから、アプリはこの変数に対する変更を監視しなければなりません（例えば、`await`を使用するなどして）。

## グループセッションへの参加

アプリでは、グループセッションに参加するためのロジックを実装する必要はありません。これは、ユーザーがFaceTime通話ウィンドウの参加ボタンをタップするからです。

ユーザーがグループセッションに参加すると、アプリの`DemoGroupActivityType.sessions()`にそのグループセッションの情報が格納されます。

![スクリーンショット 2021-12-03 20.10.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/b3110746-d190-cf36-b957-913e84cff0a9.png)

## データ送信

他の人にデータを送信するには、上記で設定した`messenger`オブジェクトを使用する。

以下のコードを`GroupActivityManager`クラスに追加する。

```swift
func send(_ message: DemoMessage) {
    if let messenger = messenger {
        Task {
            try? await messenger.send(message)
        }
    }
}
```

これで、オブジェクトの`send`関数を呼び出し、データを送信することができます。

```swift
Button("Send message") {
    manager.send(DemoMessage(id: UUID()))
}
```

## セッションデータの同期

新しいFaceTime参加者が通話に参加すると、`groupSession.$activeParticipants`ハンドラが呼び出されます（`configureGroupSession`関数で設定されます）。

通常、新しい参加者に以前のセッションデータを通知する必要があります（例えば、現在の再生時間を送信するなど）。

## データの受信

受信データの処理は`configureGroupSession`機能に実装されています。この機能では、受信データのハンドラと新しい参加者が加わる際のハンドラを設定します。

## プロジェクトの完成

これで、グループアクティビティーのホスト、グループセッションの構成、データの送受信が可能なプロジェクトが完成するはずです。

https://github.com/mszpro/SharePlay-Demo

---

iOS 15.4から、グループFaceTime通話を始めたい場合にもし進行中のFaceTime通話が無ければ、ユーザーを招待して通話を開始することができます。

https://qiita.com/MaShunzhe/items/9ff0150595bdc989593b

---

:relaxed: [Twitter @MszPro >>>](https://twitter.com/MszPro)

:newspaper: [iOS Dev Letter - iOSソフトウェア開発について取り扱う無料の月刊ニュースレター >>>](https://ios-dev-letter.com)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます >>>](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">