WWDC Quick Look 💓 By SwiftGGTeam
Integrate your custom collaboration app with Messages

Integrate your custom collaboration app with Messages

元の動画を見る

ハイライト

メッセージ チームの Devin と Lance が、iOS 16 と macOS Ventura の新しいカスタム コラボレーション API について詳しく説明します。この API セットを使用すると、サードパーティのコラボレーション アプリケーションをメッセージング アプリの会話プロセスに深く統合できます。ユーザーは、会話を離れることなく、ドキュメントを共有し、権限を設定し、アクセスを即座に確認できます。

主要内容

多くのコラボレーション アプリには、すでに独自のサーバー、権限モデル、ドキュメント リンクがあります。問題は共有の瞬間にあります。ユーザーはアプリ内のリンクをコピーし、メッセージで送信します。受信者がリンクをクリックした後、サーバーはアカウントに権限があるかどうかを確認する必要があります。グループ チャットのメンバーが変更された場合は、ドキュメントの権限を手動で同期する必要があります。

iOS 16 と macOS Ventura は、カスタム コラボレーション インフラストラクチャのシステムレベルのプロセスを補完します。まずはアプリSWCollaborationMetadata共有したいものを説明し、共有シートに渡すか、ドラッグ アンド ドロップします。ユーザーがメッセージで受信者と共有オプションを選択すると、送信前にシステムがアプリにコールバックし、ユニバーサル リンクとコラボレーション識別子を生成するように求めます (01:51)。

この遅延リンク生成の設計は重要です。リンクは、最終受信者とユーザーが選択した権限に基づいて生成できます。また、送信デバイスはアプリに受信者の一連の暗号化された ID を提供し、アプリはこれらの ID をサーバーに保存します。受信者がユニバーサル リンクを開くと、システムは署名付き ID 証明書を生成でき、サーバーによる検証後、ドキュメントのアクセス権が現在のアカウントに直接付与されます (02:50)。

セッションでは、コラボレーション メタデータの準備、コラボレーションの開始、参加者の ID の保存、アクセスの確認、メンバー変更の処理、編集およびメンション通知のメッセージへの送信など、完全なリンクについて説明します。すでにコラボレーション バックエンドがあり、ユニバーサル リンクをサポートしているアプリに適しています。

詳細

1. 使用するSWCollaborationMetadataコラボレーション内容の説明

(04:21) システムはまず、この共同共有が何を表しているのかを知る必要があります。SWCollaborationMetadataローカル識別子、タイトル、発信者のアカウント プロンプト、およびユーザーが構成可能な共有オプションを保存します。

let localIdentifier = SWLocalCollaborationIdentifier(rawValue: "identifier")
let metadata = SWCollaborationMetadata(localIdentifier: localIdentifier)
metadata.title = "Content Title"
metadata.initiatorHandle = "[email protected]"

let formatter = PersonNameComponentsFormatter()
if let components = formatter.personNameComponents(from: "Devin") {
    metadata.initiatorNameComponents = components
}

metadata.defaultShareOptions = ...

キーポイント:

  • SWLocalCollaborationIdentifierアプリがこのコンテンツをローカルで見つけられるようにするだけで、Session はデバイス間で一貫性を持たせる必要がないことを明確にします。
  • SWCollaborationMetadataこれは、後続のシートの共有、ドラッグ アンド ドロップ、およびコールバックの送信で使用されるコア オブジェクトです。
  • metadata.titleこれは、どのコンテンツが共有されているかを確認するためにユーザーに表示されます。
  • metadata.initiatorHandleそしてinitiatorNameComponentsユーザーが現在の共有アカウントを確認できるようにするためのローカル表示にのみ使用されます。
  • defaultShareOptionsデフォルトの権限設定を入力すると、ユーザーは送信前にシステム UI で変更できるようになります。

共有オプションは 3 つのオブジェクト層で構成されます。SWCollaborationOption単一のオプションを表します。SWCollaborationOptionsPickerGroup相互に排他的なオプションを示します。SWCollaborationOptionsGroup複数の独立したスイッチを表します。SWCollaborationShareOptionsこれらのグループ オプションをメタデータにフックします (06:34)。

let permission = SWCollaborationOptionsPickerGroup(identifier: UUID().uuidString,
                                                   options: [
    SWCollaborationOption(title: "Can make changes", identifier: UUID().uuidString),
    SWCollaborationOption(title: "Read only", identifier: UUID().uuidString)
])
permission.options[0].isSelected = true
permission.title = "Permission"

let additionalOptions = SWCollaborationOptionsGroup(identifier: UUID().uuidString,
                                                    options: [
    SWCollaborationOption(title: "Allow mentions", identifier: UUID().uuidString),
    SWCollaborationOption(title: "Allow comments", identifier: UUID().uuidString)
])
additionalOptions.title = "Additional Settings"
let optionsGroups = [permission, additionalOptions]
metadata.defaultShareOptions = SWCollaborationShareOptions(optionsGroups: optionsGroups)

キーポイント:

  • permission使用SWCollaborationOptionsPickerGroupの場合、ユーザーは「編集可能」と「読み取り専用」のどちらかを選択することしかできません。
  • permission.options[0].isSelected = trueデフォルトの選択を設定します。これは送信前にユーザーが変更できます。
  • additionalOptions使用SWCollaborationOptionsGroup、メンションとコメントは 2 つの独立したスイッチです。
  • 各グループとオプションには識別子があり、アプリはこれらの識別子を使用して、送信前にユーザーの最終選択を読み取ります。
  • SWCollaborationShareOptions(optionsGroups:)はいmetadata.defaultShareOptions入り口。

2. コラボレーション メタデータを共有ポータルに引き渡す

(07:58) SwiftUI は通過できますTransferableそしてShareLinkコラボレーションコンテンツを公開します。モデルオブジェクトは、SWCollaborationMetadataプロキシ表現。モデルをビューに直接渡します。ShareLink

struct CustomCollaboration: Transferable {
    var name: String

    static var transferRepresentation: some TransferRepresentation {
        ProxyRepresentation { customCollaboration in
            SWCollaborationMetadata(
                localIdentifier: .init(rawValue: "com.example.customcollaboration"),
                title: customCollaboration.name,
                defaultShareOptions: nil,
                initiatorHandle: "[email protected]",
                initiatorNameComponents: nil
            )
        }
    }
}
struct ContentView: View {
    var body: some View {
        ShareLink(item: CustomCollaboration(name: "Example"), preview: .init("Example"))
    }
}

キーポイント:

  • CustomCollaboration従うTransferableこれにより、モデル オブジェクトがシステム共有およびドラッグ アンド ドロップ プロセスに参加できるようになります。
  • ProxyRepresentation戻るSWCollaborationMetadataの場合、システムは通常のファイル コンテンツの代わりに共同メタデータを取得します。
  • localIdentifierそしてtitle現在のモデルから、アプリはコールバックを送信するときに独自のコンテンツ レコードに戻ることができます。
  • ShareLink(item:preview:)これは SwiftUI の共有の入り口であり、Session はこれを共同メタデータとともに使用します。

UIKit、AppKit、ドラッグ アンド ドロップの使用法NSItemProviderSWCollaborationMetadata従うNSItemProviderReadingそしてNSItemProviderWriting, そのため、アイテムプロバイダーに直接登録できます (09:08)。

func presentActivityViewController(metadata: SWCollaborationMetadata) {
    let itemProvider = NSItemProvider()
    itemProvider.registerObject(metadata, visibility: .all)
    let activityConfig = UIActivityItemsConfiguration(itemProviders: [itemProvider])
    let shareSheet = UIActivityViewController(activityItemsConfiguration: activityConfig)
    present(shareSheet, animated: true)
}
func createDragItem(metadata: SWCollaborationMetadata) -> UIDragItem {
    let itemProvider = NSItemProvider()
    itemProvider.registerObject(metadata, visibility: .all)
    return UIDragItem(itemProvider: itemProvider)
}

キーポイント:

  • NSItemProvider()これは、UIKit 共有システムに入る共同メタデータのコンテナです。
  • registerObject(metadata, visibility: .all)システム プロセスがこのメタデータを読み取ることを許可します。
  • UIActivityItemsConfigurationアイテムプロバイダーを共有シートに与えます。
  • UIDragItem(itemProvider:)同じメタデータで iOS および iPadOS のドラッグ アンド ドロップをサポートできるようにします。
  • セッションでは、必要に応じてメッセージが「コピーの送信」オプションを提供できるように、ファイル表現を同時に提供するなど、複数の表現を登録することをお勧めします。

3. 送信前に使用するSWCollaborationCoordinatorコラボレーションを開始する

(11:22) ユーザーが [送信] をクリックすると、メッセージが送信されますSWCollaborationCoordinatorアプリを調整して準備作業を完了します。このコーディネーターはシングルトンであり、システムはコラボレーションを処理するためにバックグラウンドでアプリを起動する可能性があるため、アプリは起動後できるだけ早くアクション ハンドラーを設定する必要があります。

private let collaborationCoordinator = SWCollaborationCoordinator.shared

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
    // Conform to the SWCollaborationActionHandler protocol
    collaborationCoordinator.actionHandler = self
}

キーポイント:

  • SWCollaborationCoordinator.sharedグローバル連携の入り口です。
  • didFinishLaunchingWithOptionsの設定actionHandler、バックグラウンド処理のコラボレーション中にデリゲートが見つからないことを避けるため。
  • ハンドラーオブジェクトは以下に準拠する必要があります。SWCollaborationActionHandler
  • セッションは、アクションをすぐに処理するように通知します。タイムアウトは送信プロセスに影響します。

最初のアクションは、SWStartCollaborationAction。ユーザーの最終選択のメタデータが含まれます。アプリはサーバーとのコラボレーションの準備をし、ユニバーサル リンクとデバイスに依存しないコラボレーション識別子を取得して、このアクションを実行します (12:27)。

func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator,
                              handle action: SWStartCollaborationAction) {
    let localID = action.collaborationMetadata.localIdentifier.rawValue
    let selectedOptions = action.collaborationMetadata.userSelectedShareOptions
    let prepareRequest = APIRequest.PrepareCollaboration(localID: localID, selectedOptions)
    Task {
        do {
            let response = try await apiController.send(request: prepareRequest)
            let identifier = response.deviceIndependentIdentifier
            action.fulfill(using: response.url, collaborationIdentifier: identifier)
        } catch {
            Log.error("Caught error while preparing the collaboration: \(error)")
            action.fail() // cancels the message
        }
    }
}

キーポイント:

  • action.collaborationMetadata.localIdentifier.rawValueアプリのローカル コンテンツに送り返す前に、システム コールバックをマップします。
  • userSelectedShareOptionsこれは、システム UI でユーザーによって最終的に選択された権限構成です。
  • APIRequest.PrepareCollaborationこれは、バックエンドに代わってこのコラボレーションのリンクと識別子を生成するサーバー側リクエストの例です。
  • action.fulfill(using:collaborationIdentifier:)また、ユニバーサル リンクとデバイスに依存しないコラボレーション識別子も引き渡します。
  • action.fail()メッセージの送信はキャンセルされます。これは、サーバー側の準備ができなかった状況に適しています。

4. 後ですぐに確認できるように参加者の ID を保存します。

(13:40) コラボレーションが正常に開始されると、システムはSWUpdateCollaborationParticipantsAction。このアクションには、参加者の暗号化 ID が含まれます。アプリが望んでいることaddedIdentities内部rootHashそれをサーバーに保存し、この共同コンテンツに関連付けます。

func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator,
                              handle action: SWUpdateCollaborationParticipantsAction) {
    let identifier = action.collaborationMetadata.collaborationIdentifier
    let participants: [Data] = action.addedIdentities.compactMap { $0.rootHash }
    let addParticipants = APIRequest.AddParticipants(identifier: identifier, participants)
    Task {
        do {
            try await apiController.send(request: addParticipants)
            action.fulfill() // sends the URL provided by the start action
        } catch {
            Log.error("Caught error while adding participants to collaboration: \(error)")
            action.fail() // cancels the message
        }
    }
}

キーポイント:

  • collaborationIdentifier前の開始アクションからの呼び出しを実行します。
  • addedIdentities受信者とコラボレーションの識別子に基づいてメッセージによって生成された ID のコレクションです。
  • rootHashはい セッションでは、サーバーに保存されるデータを指定します。このデータは、その後アクセスを認証するときに受信者を照合するために使用されます。
  • action.fulfill()パラメータを指定しない場合、メッセージは成功後に前の手順で生成された URL を送信します。
  • 失敗を招いたaction.fail()の場合、送信はキャンセルされます。

5. 受信者がリンクを開いたときに署名付き ID 証明書を生成する

(19:12) 受信機がユニバーサルリンクを開くと、アプリは最初に通過しますSWHighlightCenterコラボレーションのハイライトを見つけてもう一度呼び出しますgetSignedIdentityProof。この証明書には署名データが含まれており、サーバーはルート ハッシュを再構築する前に署名を検証します。

func application(_ app: UIApplication, open url: URL,
               options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    let highlightCenter: SWHighlightCenter = self.highlightCenter
    let challengeRequest = APIRequest.GetChallengeData()
    Task {
        do {
            let highlight = try highlightCenter.collaborationHighlight(for: url)
            let challenge = try await apiController.send(request: challengeRequest)
            let proof = try await highlightCenter.getSignedIdentityProof(for: highlight,
                                                                       using: challenge.data)
    let proofOfInclusionRequest = APIRequest.SubmitProofOfInclusion(for: proof)
            let result = try await apiController.send(request: proofOfInclusionRequest)
            documentController.update(currentDocument, with: result)
        } catch {
            Log.error("Caught error while generating proof of inclusion: \(error)")
        }
    }
}

キーポイント:

  • collaborationHighlight(for: url)現在のユニバーサル リンクが共同コンテンツに対応しているかどうかを判断します。
  • APIRequest.GetChallengeData()これは、この例ではサーバー側のチャレンジであり、リプレイ要求を防ぐために使用されます。
  • getSignedIdentityProof(for:using:)デバイスにチャレンジ データに署名させて返しますSWPersonIdentityProof
  • APIRequest.SubmitProofOfInclusion代表者は検証のために証明書をサーバーに送信します。
  • サーバー認証に合格すると、アプリは現在のドキュメントアクセスステータスを更新します。

セッションでは、サーバー側検証の数学的起源について説明します。受信側の各デバイスは暗号化された公開キーを持っています。システムはこれらの公開キーを使用してマークル ツリーを構築します。ルート ハッシュは、このコラボレーションにおける受信者の ID を表します。サーバーは署名を検証した後、Proof of Inclusion を使用してルート ハッシュを再構築し、送信時に保存されたルート ハッシュ リストと比較します (20:09)。

func generateRootHashFromArray(localHash: SHA256Digest, inclusionHashes: [SHA256Digest],
                       publicKeyIndex: Int) -> SHA256Digest {
    guard let firstHash = inclusionHashes.first else { return localHash }
    // Check if the node is the left or the right child
    let isLeft = publicKeyIndex.isMultiple(of: 2)
    // Calculate the combined hash
    var rootHash: SHA256Digest
    if isLeft {
        rootHash = hash(concatenate([localHash, firstHash]), using: .sha256)
    } else {
        rootHash = hash(concatenate([firstHash, localHash]), using: .sha256)
    }
    // Recursively pass in elements and move up the Merkle tree
    let newInclusionHashes = inclusionHashes.dropFirst()
    rootHash = generateRootHashFromArray(
        localHash: rootHash,
        inclusionHashes: Array(newInclusionHashes),
        publicKeyIndex: (publicKeyIndex / 2)
    )
    return rootHash
}

キーポイント:

  • localHash現在のデバイスの公開鍵ハッシュまたは前の層の計算結果です。
  • inclusionHashes.first包含証明によって提供された最初の兄弟ノードを取り出します。
  • publicKeyIndex.isMultiple(of: 2)現在のノードが左側にあるか右側にあるかを決定します。
  • 接合順序は左右の位置に基づいて決定する必要があります。順序が異なると、ルート ハッシュも異なります。
  • dropFirst()現在の兄弟ノードを消費し、マークル ツリーのルート ノードに向かって再帰的に移動し続けます。
  • 結局戻ってきましたrootHashサーバーに保存されている受信者のルート ハッシュと比較します。

6. グループチャットのメンバーが変わったときに権限を同期する

(24:12) コラボレーションが行われているメッセージ グループ チャットのメンバーが変更された場合、ユーザーは会話内のバナーから変更をアプリに同期できます。アプリが受け取るものはまだSWUpdateCollaborationParticipantsAction, 今回処理するのはaddedIdentitiesそしてremovedIdentities

func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator,
                              handle action: SWUpdateCollaborationParticipantsAction) {
    // Example of removing participants only. Handle the added identities here too.
    let identifier = action.collaborationMetadata.collaborationIdentifier
    let removed: [Data] = action.removedIdentities.compactMap { $0.rootHash }
    let removeParticipants = APIRequest.RemoveParticipants(identifier: identifier, removed)
    Task {
        do {
            try await apiController.send(request: removeParticipants)
            action.fulfill()
        } catch {
            log.error("Caught error while adding participants to collaboration: \(error)")
            action.fail()
        }
    }
}

キーポイント:

  • removedIdentitiesで同じものを読んでくださいrootHash、これを使用して、アクセスを取り消したい人を見つけます。
  • 削除された ID に関連付けられたアカウントが存在する場合、サーバーはそのアカウントのアクセス権を取り消します。
  • アカウントの関連付けがまだない場合、サーバーは以前に保存したルート ハッシュを削除できます。
  • コードのコメントは、この例では削除のみを示しており、完全な実装では新しい ID も処理する必要があることを示しています。
  • action.fulfill()アプリが権限の同期を完了したことを示します。

7. コラボレーションの更新情報をメッセージの会話に送り返す

(25:54) コラボレーション コンテンツが変更された後、アプリは次のことを行うことができます。SWHighlightCenter通知を発行します。コンテンツが編集されたとき、メンバーが参加または脱退したとき、誰かがメンションされたとき、またはコンテンツが移動または名前変更されたときなど、リンクが共有されている会話では、メッセージにバナーが表示されます。

func postContentEditEvent(identifier: SWCollaborationIdentifier) throws {
    let highlightCenter: SWHighlightCenter = self.highlightCenter
    let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier)

    let editEvent = SWHighlightChangeEvent(highlight: highlight, trigger: .edit)

    highlightCenter.postNotice(for: editEvent)
}

キーポイント:

  • collaborationHighlight(forIdentifier:)コラボレーション識別子を使用してシステムからハイライトを取得します。
  • SWHighlightChangeEventコンテンツの更新またはそのような変更に関するコメントを示します。
  • trigger: .editこの通知が編集イベントであることを示します。
  • postNotice(for:)イベントをシステムに渡すと、メッセージが関連する会話にイベントを表示します。

コラボレーターを参照する場合は、まずルート ハッシュを使用してコラボレーターを作成しますSWPerson.Identity、再度作成しますSWHighlightMentionEvent。セッションは、この通知が言及されたユーザーにのみ表示されることを示します (26:50)。

func postMentionEvent(identifier: SWCollaborationIdentifier, mentionedRootHash: Data) throws {
    let mentionedIdentity = SWPerson.Identity(rootHash: mentionedRootHash)

    let highlightCenter: SWHighlightCenter = self.highlightCenter
    let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier)

    let mentionEvent = SWHighlightMentionEvent(highlight: highlight,
                                               mentionedPersonIdentity: mentionedIdentity)
    highlightCenter.postNotice(for: mentionEvent)
}

キーポイント:

  • mentionedRootHash前回の検証アクセス中にユーザー アカウントに関連付けられた ID データから。
  • SWPerson.Identity(rootHash:)サーバーに保存されている ID を、システムが識別できる個人に変換します。
  • SWHighlightMentionEventバインドコラボのハイライトと言及されたアイデンティティ。
  • postNotice(for:)公開後、メッセージには、対応するユーザーへの通知のメンションのみが表示されます。

コンテンツが移動、名前変更、または削除される場合は、永続化イベントを使用します。次のコードは、名前変更トリガー (27:23) を示しています。

func postContentRenamedEvent(identifier: SWCollaborationIdentifier) throws {
    let highlightCenter: SWHighlightCenter = self.highlightCenter
    let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier)

    let renamedEvent = SWHighlightPersistenceEvent(highlight: highlight, trigger: .renamed)
    highlightCenter.postNotice(for: renamedEvent)
}

キーポイント:

  • SWHighlightPersistenceEventコンテンツの移動、名前変更、削除などのステータス変更をカバーします。
  • trigger: .renamedこの変更が名前変更であることをシステムに明示的に伝えます。
  • アプリはイベントを公開するだけでよく、メッセージによってユーザーは対応するコラボレーション コンテンツに戻ります。

重要ポイント

  • グループ チャット主導のドキュメント招待プロセスを作成: ドキュメントの共有ボタンをSWCollaborationMetadata+ シートを共有します。ユーザーがメッセージで個人と権限を選択すると、サーバーはリンクを生成します。こうすることで、リンク、権限、受信者の一貫性が保たれます。

  • アカウントをコピーせずに共同参加プロセスを作成します: 受信者がユニバーサル リンクを開いた後、次を使用します。SWHighlightCenter.getSignedIdentityProofサーバーは署名証明書を取得してルート ハッシュを再構築し、送信時に保存された ID と照合します。ドキュメント、ホワイトボード、コードレビューなどの複数人によるコラボレーション製品に適しています。

  • バックグラウンド タスクを実行して、グループ チャット メンバーの権限を自動的に同期します:SWUpdateCollaborationParticipantsAction同時に処理されるaddedIdentitiesそしてremovedIdentities。新しいメンバーを追加するとルート ハッシュに書き込まれ、メンバーを削除するとアカウントのアクセス権が取り消されるか、バインドされていないルート ハッシュが削除されます。

  • メッセージを共同で動的にリフローするための通知システムを作成します: 編集、コメント、メンション、名前変更時に個別に公開しますSWHighlightChangeEventSWHighlightMentionEventそしてSWHighlightPersistenceEvent。ユーザーは会話内でバナーを見た後、対応するコンテンツに直接戻ることができます。

  • SwiftUI ネイティブ共有モデルを作成する: 共同ドキュメント モデルを準拠させるTransferable、存在するProxyRepresentation戻ってSWCollaborationMetadata、次に使用しますShareLink露出したエントランス。このようにして、同じモデルで共有、ドラッグ アンド ドロップ、およびシステム コラボレーション プロセスを実行できます。

関連セッション

  • Shared with You をアプリに追加 — この記事では、通常の Shared with You コンテンツがアプリ内でどのように表示されるかについて説明します。SWHighlightCenterそしてアトリビューションUIの基礎。
  • メッセージによるコラボレーション エクスペリエンスの強化 - この記事では、CloudKit、iCloud Drive、およびカスタム コラボレーションをメッセージ コラボレーション UI に接続する方法について説明します。これは、このセッションのカスタム バックエンド プロセスを補完します。
  • Meet Transferable — このセッションの SwiftUI の依存関係の例TransferableそしてProxyRepresentation、このカンファレンスでは、モデル層の共有表現について体系的に説明します。
  • What’s new in SwiftUI — 本 session 使用ShareLinkSwiftUI 共有の入り口として、このセッションでは SwiftUI 2022 の関連する共有機能について説明します。

コメント

GitHub Issues · utterances