WWDC Quick Look 💓 By SwiftGGTeam
Supercharge device connectivity with Wi-Fi Aware

Supercharge device connectivity with Wi-Fi Aware

元の動画を見る

ハイライト

Wi-Fi Aware は Wi-Fi Alliance がメンテナンスするグローバル標準で、iPhone や iPad が既存の Wi-Fi インターネット接続を維持したまま、ピアツーピア・暗号化済み・低遅延のデバイス間直接接続を確立できる仕組みです。


主な内容

これまで「2 台のデバイス間でファイルを直接転送する」「アクセサリを制御する」「リアルタイムに画面を投影する」といった app を作る場合、開発者は土台となるレイヤーを自前で組み上げる必要がありました。まずデバイス発見を解決し、続いてペアリング、鍵交換、リンクの暗号化を扱い、さらにメインの Wi-Fi 帯域を奪ってユーザーをオフラインにしないよう気を配る必要もありました。結果として、各社の実装は互いに非互換で、ベンダーをまたいだ接続はほぼ不可能、バッテリーや帯域も抑え込めない状況でした。

WWDC25 で発表された Wi-Fi Aware フレームワークは、このフローを書き換えます。Wi-Fi Alliance がメンテナンスするグローバル標準をベースにしており、クロスプラットフォームかつ相互運用可能で、どのベンダーのデバイスでも参加できます。デバイスは通常の Wi-Fi 接続を維持したまま、Wi-Fi 層で動作するピアツーピアの直接接続を行い、リンク全体が自動的に認証・暗号化されます。高スループット、低遅延、複数デバイスの同時接続にも対応しています。開発者が扱うのは 2 つの上位フローだけです。Pairing(一度きりの信頼関係の構築)と Connecting(ペアリング済みデバイスへのオンデマンド接続)です。鍵交換やリンク暗号化はシステム側が担当するため、プロトコルの詳細を意識する必要はありません。

ストーリーには第二層もあります。ペアリングのエントリポイントが 2 つのフレームワークに分かれているのです。DeviceDiscoveryUI は app 同士、もしくは app からデバイスへの汎用ペアリングに適しており、Apple 製・サードパーティ製どちらのデバイスにも対応します。一方 AccessorySetupKit はアクセサリベンダー専用で、Bluetooth と Wi-Fi Aware を 1 回のフローでまとめてセットアップできます。ペアリング完了後は WAPairedDevice が手に入るので、それを Network framework の NetworkListenerNetworkBrowser に渡せば接続が確立します。


詳細

サービス宣言:Info.plist を唯一のソースに

Wi-Fi Aware の中心概念は**サービス(Service)**です。サービス名は一意である必要があり、長さは 15 文字以内、使用可能な文字は英数字とハイフンのみで、「ユニーク名 + プロトコル(tcp または udp)」の形式(例: _file-service._tcp)になります。衝突を避けるため、サービス名は IANA に登録しておくことを推奨します。

サービスには 2 つのロールがあります。Publisher(サービスを提供し接続を待ち受ける側)と Subscriber(サービスを利用しデバイスをブラウズする側)です。1 つの app が両方のロールを兼ねることもできます。Info.plistWiFiAwareServices ディクショナリで app が利用できるサービスを宣言し、Publishable あるいは Subscribable の設定でロールを決めます。app は Info.plist で宣言したサービスのみ利用できます。

宣言が済んだら、コードからは次のように能力チェックとサービスのハンドルを取得します(06:57)。

import WiFiAware

// デバイスが Wi-Fi Aware をサポートしているか確認
guard WACapabilities.supportedFeatures.contains(.wifiAware) else { return }

// Info.plist で宣言した Publishable サービス
extension WAPublishableService {
    public static var fileService: WAPublishableService {
        allServices["_file-service._tcp"]!
    }
}

// Info.plist で宣言した Subscribable サービス
extension WASubscribableService {
    public static var fileService: WASubscribableService {
        allServices["_file-service._tcp"]!
    }
    public static var droneService: WASubscribableService {
        allServices["_drone-service._udp"]!
    }
}

ポイント:

  • WACapabilities.supportedFeatures.contains(.wifiAware): 機能ゲートです。古いデバイスではここで return させ、後続の API が利用不可エラーを起こさないようにします。
  • WAPublishableService.allServices[...]: Info.plist で宣言したサービス名から publishable ハンドルを取得します。強制アンラップしているのは、ここで取れない場合は plist 側に宣言がないという開発時の不具合だからです。
  • WASubscribableService.allServices[...]: 同様に、static プロパティでサービスを包んでおくと呼び出し側から参照しやすくなります。
  • _file-service._tcp を publishable と subscribable の両方で宣言しているのは、「app がサーバーにもクライアントにもなれる」という対称的なユースケースに対応するためです。_drone-service._udp は subscribable のみで、「app が外部アクセサリを制御する」シナリオを表現しています。

ペアリングのエントリポイント:2 つの経路

DeviceDiscoveryUI は SwiftUI ビューを使ってシステム標準のペアリング UI を直接呼び出します(10:33)。

import DeviceDiscoveryUI
import WiFiAware
import SwiftUI

// Listener (Publisher) 側:アドバタイズ UI を起動
DevicePairingView(.wifiAware(.connecting(to: .fileService, from: .selected([])))) {
    // システム UI 起動前にユーザーへ表示するビュー
} fallback: {
    // エラー時のビュー
}

// Browser (Subscriber) 側:ピッカー UI を起動
DevicePicker(.wifiAware(.connecting(to: .selected([]), from: .fileService))) { endpoint in
    // ペアリング済みネットワーク endpoint を処理
} label: {
    // システム UI 起動前にユーザーへ表示するビュー
} fallback: {
    // エラー時のビュー
}

ポイント:

  • DevicePairingView(.wifiAware(.connecting(to:from:))): Publisher 側です。to でアドバタイズするサービスを指定し、from: .selected([]) ですべての発信元に対してオープンにします。
  • DevicePicker: Subscriber 側です。コールバックでネットワーク endpoint がそのまま得られるため、それを NetworkConnection に渡せます。
  • どちらのビューも fallback を受け取り、デバイス未対応や実行時エラー時のフォールバック UI を担当します。

AccessorySetupKit はアクセサリベンダー専用で、Bluetooth と Wi-Fi Aware を 1 回のフローでセットアップできます(12:29)。

import AccessorySetupKit

// ディスカバリ descriptor を設定(Subscriber)
let descriptor = ASDiscoveryDescriptor()
descriptor.wifiAwareServiceName = "_drone-service._udp"
descriptor.wifiAwareModelNameMatch = .init(string: "Example Model")
descriptor.wifiAwareVendorNameMatch = .init(string: "Example Inc", compareOptions: .literal)
let item = ASPickerDisplayItem(name: "My Drone",
                               productImage: UIImage(named: "DroneProductImage")!,
                               descriptor: descriptor)

// session を作成して有効化
let session = ASAccessorySession()
session.activate(on: sessionQueue) { event in
    // デバイス追加成功時のコールバック、event: .accessoryAdded
    // ASAccessoryWiFiAwarePairedDeviceID で WAPairedDevice を逆引き
}
// ピッカーを表示
session.showPicker(for: [item]) { error in
    // エラー処理
}

ポイント:

  • ASDiscoveryDescriptorwifiAwareServiceName / ModelNameMatch / VendorNameMatch の 3 種類のフィルタ条件で、対象のハードウェアを絞り込めます。
  • ASPickerDisplayItem はアイコン・名称・descriptor を 1 つにまとめた要素で、複数まとめて showPicker に渡すこともできます。
  • ペアリング成功時、event には ASAccessoryWiFiAwarePairedDeviceID が含まれます。これを使って WAPairedDevice を逆引きし、後続の接続で利用するエンティティを取得します。

ペアリング済みデバイスへのアクセス

ペアリングが完了すると、ペアリング済みリストは非同期シーケンスとして公開されます(13:51)。

import Foundation
import WiFiAware

var device: WAPairedDevice // WAPairedDevice.allDevices で取得

// WAPairedDevice の属性にアクセス
let pairingName = device.pairingInfo?.pairingName
let vendorName = device.pairingInfo?.vendorName
let modelName = device.pairingInfo?.modelName

// フィルタを作成
let filter = #Predicate<WAPairedDevice> {
    $0.pairingInfo?.vendorName.starts(with: "Example Inc") ?? false
}

// 条件に一致するすべてのペアリング済みデバイスを取得
// デバイスの追加・削除・変更のたびに新しいスナップショットが届く
for try await devices in WAPairedDevice.allDevices(matching: filter) {
    // 新しいスナップショットを処理
}

ポイント:

  • WAPairedDevice.allDevices(matching:)AsyncSequence を返し、ペアリングリストに変更があるたびに全件のスナップショットがプッシュされます。UI を駆動するのに便利です。
  • #Predicate<WAPairedDevice> は Swift Predicate マクロで、フィルタ条件をコンパイル時に確定させます。
  • ユーザーが設定 app からペアリングを削除する可能性があるため、app はこのシーケンスの変更に応じて自身の状態を更新する必要があります。

接続の確立:Network framework に任せる

サービス宣言とペアリングが済めば、接続自体は Network framework の標準 API に基づきます(16:54)。

import WiFiAware
import Network

// Listener (Publisher) 側:NetworkListener を構築
let listener = try NetworkListener(for:
        .wifiAware(.connecting(to: .fileService, from: .matching(deviceFilter))),
    using: .parameters {
        TLS()
    })
    .onStateUpdate { listener, state in
        // 状態更新
    }

// Browser (Subscriber) 側:NetworkBrowser を構築
let browser = NetworkBrowser(for:
        .wifiAware(.connecting(to: .matching(deviceFilter), from: .fileService))
    )
    .onStateUpdate { browser, state in
        // 状態更新
    }

ポイント:

  • NetworkListener(for: .wifiAware(...)): Wi-Fi Aware サービスを通常の Network framework の listener にラップします。TLS などのパラメータも Network framework の標準的な書き方そのままです。
  • from: .matching(deviceFilter): Listener はフィルタで接続を受け付ける相手を制限し、Browser はフィルタで探索対象のデバイスを制限します。
  • .onStateUpdate: 下層の無線リソースが ready / running / failed / cancelled のどの状態にあるかを観測します。「つながらない」を診断する第一歩はここです。

パフォーマンスチューニング

デフォルト設定は .bulk + .bestEffort で、ファイル転送系のワークロードに向いています。動画や制御系で低遅延が求められる場合は .realtime + .interactiveVideo に切り替えます(21:11)。

// Listener (Publisher) 側
let listener = try NetworkListener(for:
        .wifiAware(.connecting(to: .fileService, from: .matching(deviceFilter))),
    using: .parameters {
        TLS()
    }
    .wifiAware { $0.performanceMode = .realtime }
    .serviceClass(.interactiveVideo))

// Browser (Subscriber) 側
let connection = NetworkConnection(to: endpoint, using: .parameters {
        TLS()
    }
    .wifiAware { $0.performanceMode = .realtime }
    .serviceClass(.interactiveVideo))

// 両端でパフォーマンスレポートを読み取れる
let performanceReport = try await connection.currentPath?.wifiAware?.performance

ポイント:

  • performanceMode = .realtime: 低遅延モードです。電力コストが高くなるため、インタラクティブな場面に限って有効化することを推奨します。
  • serviceClass(.interactiveVideo): QoS レベルを指定し、この種のトラフィックを優先的にスケジュールさせます。ほかに .voice / .background / .bestEffort が選べます。
  • connection.currentPath?.wifiAware?.performance: スループット、遅延、信号強度などの実行時メトリクスを取得できます。UI で「もう少し近づいてください」とユーザーに促す材料にも使えます。

主な学び

1. app に「近距離デバイス直接接続」のスイッチを追加する

なぜ取り組む価値があるか: iPad / iPhone での協調シナリオ(ホワイトボード、ファイル共有、リアルタイム投影)の多くは、これまでクラウド経由かローカルの Wi-Fi サービスディスカバリに頼っており、ルーターやネットワーク分離の影響を強く受けていました。Wi-Fi Aware は Wi-Fi 層でピアツーピアを成立させるため、NAT や企業ネットワークの制限を回避できます。

どう始めるか: Info.plist で publishable + subscribable の対称的なサービス名を宣言し、まずは DeviceDiscoveryUI で最小のペアリング demo を通します。そのうえで既存の URLSession を、endpoint ベースの NetworkConnection に置き換えていきます。

2. アクセサリ系の app は AccessorySetupKit で Bluetooth + Wi-Fi Aware を一度に決める

なぜ取り組む価値があるか: ハードウェアアクセサリは、Bluetooth で発見・制御を行い、Wi-Fi で大きなデータを流す構成がよくあります。従来はペアリングを 2 段に分ける必要があり、体験が分断していました。AccessorySetupKit はこの 2 つのリンクをまとめ、ユーザーは PIN コード確認を 1 回行うだけで済みます。

どう始めるか: ASDiscoveryDescriptor を埋める際に Bluetooth と wifiAwareServiceName / VendorNameMatch / ModelNameMatch を同時に設定し、初回セットアップの段階で完全な機能をユーザーに渡せるようにします。

3. 動画 / 制御系のシナリオは .realtime + .interactiveVideo に切り替える

なぜ取り組む価値があるか: デフォルトの .bulk + .bestEffort は複数デバイス共存時に後ろに並ばされ、平均遅延は小さくてもジッタが大きくなりがちです。ドローン制御、リモートカメラのプレビュー、AR のリアルタイム同期などはジッタに敏感です。

どう始めるか: NetworkListener / NetworkConnection.parameters クロージャに .wifiAware { $0.performanceMode = .realtime }.serviceClass(.interactiveVideo) を追加し、UI 側では performanceReport を読み出してリンク品質をリアルタイムにフィードバックします。

4. ペアリング済みデバイスのリスト UI は WAPairedDevice.allDevices で駆動する

なぜ取り組む価値があるか: ユーザーはいつでもシステム設定からペアリングを削除できます。allDevicesAsyncSequence であり、変更のたびに全件スナップショットがプッシュされるため、自前でローカルキャッシュを持つよりも信頼性の高い設計になります。

どう始めるか: デバイス管理用の SwiftUI ビューで .task を使って for try await devices in WAPairedDevice.allDevices(matching: filter) を一度起動し、devices@State に書き込めば UI が自動的に追従します。


関連 Session

  • Meet AccessorySetupKit — WWDC24 のアクセサリペアリングフロー公式セッション。Wi-Fi Aware をアクセサリ用途で使う際の前提となる内容です。
  • What’s new in Network framework — Network framework の新機能。Wi-Fi Aware の Listener / Browser / Connection はこの上に構築されています。
  • Integrate privacy into your development process — プライバシーと権限を開発プロセスに組み込む話で、Wi-Fi Aware のペアリング認可モデルとも相性のよい内容です。
  • What’s new in passkeys — 同じ「プライバシー / セキュリティ」テーマのセッション。passkey も Wi-Fi Aware も「クレデンシャルはデバイスにとどめ、クロスプラットフォームの標準で繋ぐ」という方向性を体現しています。

コメント

GitHub Issues · utterances