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

Explore Nearby Interaction with third-party accessories

观看原视频

Highlight

iOS 15 让 Nearby Interaction 支持兼容的第三方 UWB 配件,开发者可以用 NINearbyAccessoryConfiguration 从配件获取精确距离和方向更新,解决蓝牙等通道难以判断近距离空间关系的问题。

核心内容

你做了一个智能音箱、工具箱标签或展厅装置。用户走近 3 米时,App 想显示基础控制;走进 1.5 米时,App 想打开更精确的操作界面。

以前很难做稳。蓝牙可以负责连接和传输数据,但它不适合判断用户在房间里的精确位置。网络连接也能交换状态,却无法告诉 App 用户站在配件的哪个方向。

Nearby Interaction(附近交互)在 iOS 14 已经能让两台搭载 U1 芯片的 iPhone 交换距离和方向信息。问题是,第三方配件无法直接加入这个会话。

iOS 15 补上了这个缺口。Apple 新增 NINearbyAccessoryConfiguration,让兼容的第三方 UWB(Ultra Wideband,超宽带)硬件可以和 iPhone 的 U1 芯片互操作。

这个能力不替代蓝牙或网络。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)第三方配件会先通过蓝牙、本地网络或安全的互联网连接,把 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 抽象数据通道,可以由蓝牙、局域网或其他安全连接实现。
  • 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 表示一个或多个附近对象被移除。
  • 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 米半径。用户进入 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 不必依赖蓝牙信号强度猜测用户是否走近。 怎么开始:让音箱通过蓝牙发送 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 且重试次数未超限时,用缓存的 NINearbyAccessoryConfiguration 再次调用 session.run(_:)

  5. 做什么:在 App 中设计清晰的 Nearby Interaction 权限入口。 为什么值得做:iOS 15 只在第一次运行 NISession 时弹出权限提示,时机不清楚会降低用户授权概率。 怎么开始:把 session.run(_:) 放在“开始查找配件”按钮之后,在 Info.plist 的 NSNearbyInteractionUsageDescription 中说明距离感知用途。

关联 Session

评论

GitHub Issues · utterances