WWDC Quick Look 💓 By SwiftGGTeam
Explore Nearby Interaction with third-party accessories

Explore Nearby Interaction with third-party accessories

元の動画を見る

ハイライト

iOS 15 では Nearby Interaction が互換性のあるサードパーティ UWB アクセサリに対応し、開発者は NINearbyAccessoryConfiguration を使ってアクセサリから正確な距離と方向の更新を受け取れるようになります。Bluetooth などのチャネルだけでは近距離の空間関係を判定しにくい課題を解決します。

主要内容

スマートスピーカー、工具箱タグ、展示会場のデバイスを作っているとします。ユーザーが 3 メートル以内に近づいたら App は基本操作を表示し、1.5 メートル以内に入ったらより精密な操作画面を開きたいはずです。

従来はこれを安定して実現するのが困難でした。Bluetooth は接続とデータ転送を担えますが、ユーザーが部屋のどこにいるかを正確に判断する用途には向きません。ネットワーク接続でも状態は交換できますが、ユーザーがアクセサリのどちら側に立っているかまでは App に伝えられません。

Nearby Interaction は iOS 14 の時点で、U1 チップ搭載 iPhone 同士が距離と方向の情報を交換できるようにしていました。課題は、サードパーティアクセサリがこのセッションに直接参加できなかったことです。

iOS 15 はこの不足を埋めます。Apple は NINearbyAccessoryConfiguration を追加し、互換性のあるサードパーティ UWB(Ultra Wideband)ハードウェアが iPhone の U1 チップと相互運用できるようにしました。

この機能は Bluetooth やネットワークを置き換えるものではありません。App は引き続きデータチャネルを使い、iPhone とアクセサリの間で構成データをやり取りします。Nearby Interaction は UWB 測距セッションを担当し、データチャネルはセッションの開始と調整を担当します。

アクセサリはまず Accessory Configuration Data を生成します。App はそれを受け取って NINearbyAccessoryConfiguration を作成し、NISession を開始します。その後、システムは delegate 経由で Shareable Configuration Data を生成するため、App はできるだけ早くアクセサリへ送り返す必要があります。双方の構成が完了すると、App は距離を含む NINearbyObject の更新を受け取り、ハードウェアが対応している場合は方向も受け取ります。

詳細

1. 権限は一度限りの許可から使用中の許可へ

02:59)iOS 14 の Nearby Interaction 権限は、App のライフサイクルごとに一度だけ許可される仕組みでした。ユーザーが次に関連フローへ入り直すと、再びプロンプトが表示される可能性がありました。

03:34)iOS 15 のプロンプトは「App の使用中」の許可に変わりました。システムは App が初めて NISession を実行したときに自動でプロンプトを表示します。ユーザーが許可または拒否した後、同じプロンプトは再表示されません。その後、ユーザーは Settings で権限を変更できます。

<key>NSNearbyInteractionUsageDescription</key>
<string>アクセサリに近づいたときに距離に応じたコントロールを表示するために使います。</string>

キーポイント:

  • NSNearbyInteractionUsageDescription は Info.plist 内の用途説明です。
  • システムはこの文言を Nearby Interaction の権限プロンプトに表示します。
  • 初めて NISession を実行するタイミングは、「アクセサリに接続」や「検索を開始」など明確なユーザー操作に対応させます。
  • 権限が不足している場合、NISession は権限関連エラーで無効になるため、App は理由を説明し、Settings で変更するよう案内します。

2. アクセサリデータから NINearbyAccessoryConfiguration を作成する

08:43)サードパーティアクセサリはまず Bluetooth、ローカルネットワーク、または安全なインターネット接続を通じて Accessory Configuration Data を App に送ります。このデータは、U1 互換の UWB ハードウェアが定められた形式で生成します。

10:45)App はデータを受け取ったら、それを使って NINearbyAccessoryConfiguration を作成します。データ形式が無効な場合、初期化時にエラーが送出されます。

import Foundation
import NearbyInteraction

final class AccessoryStore {
    private var namesByToken: [NIDiscoveryToken: String] = [:]

    func setupAccessory(configurationData: Data, name: String) throws -> NINearbyAccessoryConfiguration {
        let configuration = try NINearbyAccessoryConfiguration(data: configurationData)
        namesByToken[configuration.accessoryDiscoveryToken] = name
        return configuration
    }

    func name(for token: NIDiscoveryToken) -> String? {
        namesByToken[token]
    }
}

キーポイント:

  • import NearbyInteraction は Nearby Interaction フレームワークを取り込みます。
  • setupAccessory(configurationData:name:) はアクセサリから届いた構成データとアクセサリ名を受け取ります。
  • try NINearbyAccessoryConfiguration(data:) はデータ形式を検証し、アクセサリセッション用の構成を生成します。
  • configuration.accessoryDiscoveryToken はフレームワークが設定するアクセサリ発見識別子です。
  • namesByToken は発見識別子とアクセサリ名を関連付け、後で NINearbyObject の更新を受け取ったときに正しいアクセサリ名を表示できるようにします。
  • メソッドは NINearbyAccessoryConfiguration を返し、呼び出し側はそれを使って NISession を開始できます。

3. NISession を開始し、Shareable Configuration Data をアクセサリへ返す

13:02)App は構成を作成した後、NISession を作成し、delegate を設定して run(_:) を呼び出します。

13:19)アクセサリ側にも Nearby Interaction からの Shareable Configuration Data が必要です。システムは iOS 15 で追加された delegate コールバックを通じてそれを App に渡します。App は元のデータチャネルでできるだけ早くアクセサリへ送る必要があり、遅延が大きいとセッションがタイムアウトします。

import Foundation
import NearbyInteraction

protocol AccessoryConnection {
    func send(_ data: Data)
}

final class AccessorySessionController: NSObject, NISessionDelegate {
    private let session = NISession()
    private let configuration: NINearbyAccessoryConfiguration
    private let connection: AccessoryConnection

    init(configuration: NINearbyAccessoryConfiguration, connection: AccessoryConnection) {
        self.configuration = configuration
        self.connection = connection
        super.init()
        session.delegate = self
    }

    func start() {
        session.run(configuration)
    }

    func session(
        _ session: NISession,
        didGenerateShareableConfigurationData shareableConfigurationData: Data,
        for object: NINearbyObject
    ) {
        connection.send(shareableConfigurationData)
    }
}

キーポイント:

  • AccessoryConnection はデータチャネルを抽象化し、Bluetooth、ローカルネットワーク、その他の安全な接続で実装できます。
  • NISession() は Nearby Interaction セッションを作成します。
  • session.delegate = self により、コントローラがフレームワークのコールバックを受け取ります。
  • session.run(configuration) はアクセサリ構成でセッションを開始します。
  • didGenerateShareableConfigurationData は Shareable Configuration Data を提供します。
  • connection.send(shareableConfigurationData) はデータをそのままアクセサリへ送り返します。
  • for object: NINearbyObject はこのデータがどのアクセサリに属するかを示し、複数アクセサリを並行扱いする場合に重要です。

4. タイムアウトを処理し、構成がまだ有効なら再試行する

15:25)Shareable Configuration Data がアクセサリへ時間内に届かないと、セッションがタイムアウトすることがあります。Nearby Interaction は didRemove delegate コールバックで App に通知します。

15:44)App は削除理由を確認できます。理由が .timeout で、アクセサリがまだ近くにあると App が判断できる場合、同じ構成で session を再実行できます。キャッシュした構成が有効なのは、アクセサリ側の session が終了していない場合だけです。

import NearbyInteraction

final class RetryController: NSObject, NISessionDelegate {
    private let session: NISession
    private let configuration: NINearbyAccessoryConfiguration
    private var retryCount = 0

    init(session: NISession, configuration: NINearbyAccessoryConfiguration) {
        self.session = session
        self.configuration = configuration
        super.init()
    }

    func session(
        _ session: NISession,
        didRemove nearbyObjects: [NINearbyObject],
        reason: NINearbyObject.RemovalReason
    ) {
        guard reason == .timeout else { return }
        guard shouldRetry() else { return }

        retryCount += 1
        session.run(configuration)
    }

    private func shouldRetry() -> Bool {
        retryCount < 3
    }
}

キーポイント:

  • didRemove nearbyObjects は 1 つ以上の近くのオブジェクトが削除されたことを示します。
  • reason == .timeout はセッションのタイムアウトに対応します。
  • shouldRetry() には、再試行回数やアクセサリから停止通知が来たかどうかなど、App 独自の判断ロジックを置きます。
  • retryCount += 1 は現在の再試行回数を記録し、無限リトライを避けます。
  • session.run(configuration) は同じ構成でセッションを再度開始します。
  • アクセサリ側の session が終了している場合、App は Accessory Configuration Data を再取得し、開始フロー全体をやり直す必要があります。

5. 距離更新でゾーン機能を駆動する

19:24)iPhone とアクセサリの両方で構成が完了すると、App は didUpdate を通じて NINearbyObject の更新を受け取ります。更新には距離が含まれ、ハードウェアが対応している場合は方向も含まれます。

20:26)セッション内のシナリオでは、半径 1.5 メートルと 3 メートルの 2 つのゾーンを定義しています。ユーザーが 3 メートル圏内に入ると機能 A を有効にし、1.5 メートル圏内に入ると機能 B を有効にします。Apple は、ユーザーが急に動いたり境界付近に立ったりしたときに UI が激しく切り替わらないよう、距離を平滑化することを推奨しています。

import NearbyInteraction

final class DistanceZoneController: NSObject, NISessionDelegate {
    private var lastSmoothedDistance: Float?

    func session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObject]) {
        guard let object = nearbyObjects.first else { return }
        guard let distance = object.distance else { return }

        let smoothedDistance = getSmoothedDistance(distance)

        if smoothedDistance < 1.5 {
            enableFunctionalityB()
        } else if smoothedDistance < 3.0 {
            enableFunctionalityA()
        }
    }

    private func getSmoothedDistance(_ distance: Float) -> Float {
        guard let previous = lastSmoothedDistance else {
            lastSmoothedDistance = distance
            return distance
        }

        let alpha: Float = 0.3
        let smoothed = alpha * distance + (1 - alpha) * previous
        lastSmoothedDistance = smoothed
        return smoothed
    }

    private func enableFunctionalityA() {
        // ユーザーが 3 メートル圏内に入った後の機能。
    }

    private func enableFunctionalityB() {
        // ユーザーが 1.5 メートル圏内に入った後の機能。
    }
}

キーポイント:

  • didUpdate nearbyObjects は距離と方向の更新入口です。
  • nearbyObjects.first は今回更新されたアクセサリオブジェクトを取り出します。
  • object.distance はフレームワークが提供する距離で、単位はメートルです。
  • getSmoothedDistance(_:) は距離を平滑化し、境界付近での UI の揺れを減らします。
  • smoothedDistance < 1.5 はより近いゾーンに対応し、機能 B をトリガーします。
  • smoothedDistance < 3.0 は広い側のゾーンに対応し、機能 A をトリガーします。
  • 複数アクセサリを並行して扱う場合は、アクセサリごとに session を作成して実行できます。

重要ポイント

  1. 何をするか:スマートスピーカーに「近づくとコントロールパネルを表示する」機能を追加します。 取り組む価値:Nearby Interaction はアクセサリとの正確な距離を提供できるため、App は Bluetooth 信号強度に頼ってユーザーが近づいたかを推測する必要がありません。 始め方:スピーカーから Bluetooth で Accessory Configuration Data を送り、App は NINearbyAccessoryConfiguration(data:)NISession を開始し、didUpdate 内で 3 メートルのしきい値を使ってコントロールパネルを表示します。

  2. 何をするか:倉庫の工具タグに「近距離確認」を追加します。 取り組む価値:UWB の距離は、ユーザーが本当に対象アクセサリのそばに立っているかを判断するのに適しており、誤操作を減らせます。 始め方:タグごとに accessoryDiscoveryToken から名前へのマッピングを保持し、object.distance が 1.5 メートル未満になったら「受け取りを確認」ボタンを表示します。

  3. 何をするか:展示会場 App を作り、ユーザーが展示物に近づいたら内容を自動で切り替えます。 取り組む価値:セッションで示された 1.5 メートルと 3 メートルのゾーンモデルは、遠くでは概要、近くでは操作を見せる展示説明の階層にそのまま使えます。 始め方:展示物ごとに互換 UWB アクセサリを用意し、App はアクセサリごとに session を作成し、didUpdate 内で距離しきい値に応じてページ状態を切り替えます。

  4. 何をするか:アクセサリ接続フローに復帰可能なタイムアウト再試行を追加します。 取り組む価値:Shareable Configuration Data の送信が遅すぎると session がタイムアウトするため、再試行によりユーザーが再接続する回数を減らせます。 始め方session(_:didRemove:reason:) を実装し、reason == .timeout かつ再試行回数が上限未満なら、キャッシュした NINearbyAccessoryConfigurationsession.run(_:) を再度呼び出します。

  5. 何をするか:App 内に明確な Nearby Interaction 権限入口を設計します。 取り組む価値:iOS 15 は初回の NISession 実行時だけ権限プロンプトを表示するため、タイミングが分かりにくいとユーザーが許可する確率が下がります。 始め方session.run(_:) を「アクセサリの検索を開始」ボタンの後に置き、Info.plist の NSNearbyInteractionUsageDescription で距離検知の用途を説明します。

関連セッション

  • Design for spatial interaction — 距離と方向を、ユーザーが理解できる空間インタラクションへ変換する方法を学べます。
  • Explore UWB-based car keys — UWB が車のキーのシナリオで近接、解錠、パーソナライズを実現する仕組みを確認できます。
  • Meet the Location Button — iOS 15 における低摩擦な位置情報許可設計と比較できます。
  • Connect Bluetooth devices to Apple Watch — Bluetooth アクセサリの接続方式を参考にし、Nearby Interaction に必要なデータチャネルを理解できます。

コメント

GitHub Issues · utterances