1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nearby Interaction × ARViewでデバイス間の距離を吹き出しに表示してみた

1
Last updated at Posted at 2025-12-23

Nearby Interactionとは

WWDC20で登場したUWB(Ultra Wideband)チップを利用して、近くにある対応デバイス同士の距離と方向をアプリ側で取得できるようにする フレームワークです。
BluetoothやBeaconが「近い・遠い」といった大まかな距離感を扱うのに対し、Nearby Interaction は UWB(Ultra Wideband) を利用することで、数十センチ単位の距離や相手がどの方向にいるのかまでを高精度に取得できます。
「検知する」から「空間的な位置関係を理解する」へと進化した点が、Nearby Interaction の大きな特徴です。

この特性から、Nearby Interaction はすでにさまざまな分野で応用されています。
例えば、AirTag に代表される精密な探索体験など、ユーザーの「近くにあるもの」を自然に扱う体験を支えています。

UWBとは

超広帯域無線通信(Ultra Wideband)の略で、超広帯域の周波数帯域幅を利用する無線通信のことです。歴史は古く1960年代に軍事用として開発された技術になります。距離と方向を取得する以外にも、非接触のバイタルセンサーや非破壊検査など、様々な分野に応用が進められています。
https://www.soumu.go.jp/main_content/000692221.pdf

近距離通信・近接検知に使われてきた代表的な技術であるBluetooth・赤外線と、障害物への適応や得意な領域という観点で整理します。

技術 障害物への強さ 理由 得意な領域
UWB(Ultra Wideband) 比較的強い 非常に短いパルスを広帯域で送信し、到達時間(Time of Flight)を直接測定するため、反射やノイズの影響を分離しやすい ・高精度な距離・方向測定
・屋内測位
・精密な近接インタラクション
Bluetooth 普通 電波強度(RSSI)を距離の目安として使うため、壁や人などの障害物で減衰・反射の影響を受けやすい ・デバイス検出
・低消費電力通信
・汎用的な近接検知
赤外線 弱い 光通信のため直進性が高く、障害物があると信号が遮断されてしまう ・リモコン操作
・短距離・一点方向の通信

ARViewとは

iOS で AR(拡張現実)体験を表示・操作するためのビュー です。
RealityKit フレームワークの中核で、カメラ映像を背景にして 3D オブジェクトを現実空間に重ねたり、デバイスの動きに応じて AR コンテンツを動かしたりするために使われます。
Nearby Interaction は、AR(拡張現実)と組み合わせることで真価を発揮します。
取得した距離や方向の情報をAR空間に重ねることで、相手デバイスに向かって「どの方向に動けばいいのか」が直感的に分かるようになります。

矢印や吹き出しを表示することで、現実空間とデジタル情報が自然に結びついた、新しいユーザー体験を設計することが可能になります。

Nearby Interaction × ARViewの可能性

Nearby InteractionとARViewを組み合わせると、従来の点検業務に大きな変化をもたらす可能性があります。
例えば、作業者が設備や機器にカメラを向けるだけで、Nearby Interactionによって その機器までの距離や方向を認識し、AR上に状態情報や操作用の吹き出しを表示 することができます。

これにより、従来の手作業による数値の確認やぶ厚い説明書を読まずとも、目の前の機器の状態を直感的に把握できるようになると考えます。
つまり、Nearby Interactionによる位置認識とARViewによる可視化を組み合わせることで、「カメラを向けるだけで情報を確認できるスマート点検体験」が実現できると考えました。
Designer (1).png

Nearby Interactionを触ってみる

iPhone 12 と iPhone 12 mini を使用し、UWBチップ搭載デバイス同士の距離と方向の取得を試しました。作成したアプリでは、2台のデバイス間の距離や方向をリアルタイムで取得し、ARView 上に 吹き出しに距離と座標の可視化を行いました。
実装は、「【Swift】Nearby Interactionを用いたiPhone間の距離と方向を取得するアプリ」を参考にさせていただきました。

実装

私自身の頭の整理も兼ねて、実装部分をかいつまんで説明します。

Multipeer Connectivityでトークンを交換する

NIDiscoveryToken (Nearby Interaction Discovery Token) は、Nearby Interactionフレームワークで、相互作用セッションに参加するピア(デバイス)を識別するためのユニークなIDです。このトークンをデバイス間で交換することで、近接したデバイス同士が互いを認識し、距離や方向の測定(高精度な近接検知)を開始できるようになります。
Multipeer Connectivityについては別記事で紹介しました。
Multipeer Connectivityを使用して共有できるメモアプリ作成してみた

extension UWBManager: MCSessionDelegate {

    // ピア接続時に NIDiscoveryToken を送る
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {

        DispatchQueue.main.async {
            switch state {
            case .connected:
                self.connectionStatus = "接続成功(\(peerID.displayName))"

                // DiscoveryToken を送る処理はそのまま ↓
                if let token = self.niSession.discoveryToken,
                   let data = try? NSKeyedArchiver.archivedData(
                        withRootObject: token,
                        requiringSecureCoding: true
                   ) {
                    try? session.send(data, toPeers: [peerID], with: .reliable)
                }

            case .connecting:
                self.connectionStatus = "接続中…"

            case .notConnected:
                self.connectionStatus = "未接続"

            @unknown default:
                self.connectionStatus = "未知の状態"
            }
        }
    }

NISessionを開始する

トークンを受信したら、startNI(with: token)でセッションを開始します。

// トークンを受信した時の処理
func session(_ session: MCSession,
             didReceive data: Data,
             fromPeer peerID: MCPeerID) {

    if let token = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NIDiscoveryToken.self, from: data) {
        DispatchQueue.main.async { self.startNI(with: token) }
    }
}


func startNI(with token: NIDiscoveryToken) {
    let config = NINearbyPeerConfiguration(peerToken: token)
    niSession.run(config)
}

NISessionDelegateで、相手デバイスの距離や方向の情報を受け取る

session(_:didUpdate:)は、近くのオブジェクトに関する情報が更新するたびに呼び出されます。
今回は1対1の接続としているため.firstで値を取得していますが、接続しているオブジェクトが複数ある場合はNIDiscoveryTokenを使うことで、それぞれのオブジェクトを判定できます。

@Published var latestDistance: Float?
@Published var latestDirection: simd_float3?

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

    DispatchQueue.main.async {
        self.latestDistance = obj.distance
        self.latestDirection = obj.direction
    }
}

ARViewに反映する

@Published属性で監視しているlatestDistanceとlatestDirectionが更新された際に吹き出しを更新します。

// MARK: - 移動平均+補間で滑らかに距離表示
private func updateDistanceBubble(distance: Float, direction: simd_float3) {
    // 推定座標(メートル単位)
    let pos = direction * distance

    // ① 移動平均バッファに追加
    recentPositions.append(pos)
    if recentPositions.count > maxPositions {
        recentPositions.removeFirst()
    }

    // ② 移動平均を計算
    let avgPos = recentPositions.reduce(SIMD3<Float>(0,0,0), +) / Float(recentPositions.count)

    // ③ 前フレーム位置との線形補間でさらに滑らかに
    let smoothedPos: SIMD3<Float>
    if let last = lastPosition {
        let alpha: Float = 0.2 // 補間係数(小さいほど滑らか)
        smoothedPos = last + alpha * (avgPos - last)
    } else {
        smoothedPos = avgPos
    }
    lastPosition = smoothedPos

    // 3D距離を計算
    let distance3D = sqrt(smoothedPos.x * smoothedPos.x +
                          smoothedPos.y * smoothedPos.y +
                          smoothedPos.z * smoothedPos.z)

    coordinateLabel.text = String(format: "推定座標\nx: %.2f  y: %.2f  z: %.2f",
                                  pos.x, pos.y, pos.z)
    // 吹き出しに表示
    bubbleLabel?.text = String(format: "%.2f m\nx: %.2f\ny: %.2f\nz: %.2f", distance3D,pos.x, pos.y, pos.z)

    // 吹き出しを3D位置に追従
    updateBubble(position: smoothedPos)
}

private func updateBubble(position: SIMD3<Float>) {
    // 3D → 2D に投影
    let projected = arView.project(position)
    // カメラからの実距離(m)
    let distance = simd_length(position)
    let minDistance: Float = 0.2
    let maxDistance: Float = 1.0
    let maxScale: CGFloat = 1.0
    let minScale: CGFloat = 0.4
    if let p = projected {
        bubbleLabel.center = CGPoint(
            x: CGFloat(p.x),
            y: CGFloat(p.y)
        )
        // 遠くにある場合は小さく、近くにある場合は大きく見せるための処理
        let clampedDistance = max(min(distance, maxDistance), minDistance)
        let t = (clampedDistance - minDistance) / (maxDistance - minDistance)
        let scale = maxScale - CGFloat(t) * (maxScale - minScale)

        // 幅・高さを変更(center は維持)
        let newSize = CGSize(
            width: bubbleBaseSize.width * scale,
            height: bubbleBaseSize.height * scale
        )
        // フォントサイズ変更
        let newFontSize = bubbleBaseFontSize * scale
        bubbleLabel.font = bubbleLabel.font.withSize(newFontSize)

        bubbleLabel.bounds.size = newSize
        
        bubbleLabel.isHidden = false
    } else {
        // カメラ後方などで見えない場合
        bubbleLabel.isHidden = true
    }
}

デモアプリ動作

Multipeer Connectivityでトークンを交換する

NI1Filename.gif

ARViewの表示

NI2Filename.gif

気づいた点

同じ位置に固定していても座標は更新される

2端末を同じ位置に固定している場合でも細かいレベルで座標は更新されます。以下のグラフは、端末間に約50cmの距離を保った状態で、約30秒間計測した結果を示しています。
NIsessionDistance.png

このことから、細かな誤差は無視するような処理でないと常にARViewで表示されるオブジェクトが動いてしまい、画面酔いする原因となることに気づきました。

まとめ

今回はiPhone端末同士で、Nearby Interaction × ARViewを試してみました。
Nearby InteractionではUltra Wideband(UWB)対応のサードパーティ製アクセサリとの連携も可能です。

  • センサーデータ収集:
    各種センサー機器が MQTT を用いてデータを送信
    AWS IoT Core などのクラウドで集約・管理
  • 端末側表示
    iPhone / iPad の Nearby Interaction により周辺デバイスとの相対位置・距離を取得
    ARView により、AR空間上に情報を重ねて表示

上記のように構成することで、Nearby Interaction × ARViewの可能性で示したような「カメラを向けるだけで情報を確認できるスマート点検体験」が実現できると考えています。

参考記事

Nearby Interaction | Apple Developer Documentation
UWB(超広帯域)無線通信とは
【Swift】Nearby Interactionを用いたiPhone間の距離と方向を取得するアプリ

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?