WWDC Quick Look 💓 By SwiftGGTeam
Design for spatial interaction

Design for spatial interaction

观看原视频

Highlight

Apple 在 iOS 15 中把 U1 空间感知和 Nearby Interaction 能力开放给第三方硬件,开发者可以设计跨设备、带距离和方向反馈的近场交互。

核心内容

过去,很多跨设备操作都要靠列表完成。

你想把 iPhone 上的音乐切到客厅的 HomePod,需要打开 AirPlay 列表。列表里每个扬声器都排在一起,近处的设备和远处的设备看起来没有差别。用户真正想表达的是“把这首歌放到这个扬声器上”,界面却要求他先读列表,再选择名称。

找东西也类似。地图只能把你带到大致位置。到了房间里,你还要靠眼睛、耳朵和猜测继续找。对于藏在沙发缝、包里、桌子底下的物品,屏幕上的抽象位置帮助有限。

Apple 在这场 session 里总结了 AirTag、HomePod mini 和 iPhone 的设计经验。核心变化来自 U1 芯片带来的空间感知,以及 iOS 15 中面向第三方硬件的新 API。设备可以理解附近物体的距离、方向和相对关系,应用可以把这些信息转成视觉、声音和触觉反馈。

这改变了交互的入口。用户不必先理解列表、按钮和设备名称。应用可以让用户移动身体、靠近目标、转向目标,然后用连续反馈告诉他下一步该做什么。

距离决定界面

AirTag 的查找体验展示了第一个原则:同一个任务在不同距离下需要不同界面。

距离很远时,地图和路线有用。进入附近范围后,按钮从“获取路线”变成“查找”。连接成功后,箭头开始指向目标。靠近到手臂范围内时,箭头让位给触觉反馈,因为毫米级移动更适合用手感判断。

设计近场交互时,不要把一个界面从头用到尾。远距离需要方向。近距离需要精度。到了最后几厘米,屏幕的优先级会下降。

反馈必须连续

HomePod mini 的音乐传输展示了第二个原则:反馈要跟着身体动作连续变化。

iPhone 靠近 HomePod mini 时,屏幕上的横幅位置、大小和背景模糊会随距离变化。HomePod 顶部灯光也会响应靠近动作。用户继续靠近会确认传输,拉远会取消操作。

这里没有额外的“确认”按钮。身体动作本身承担了确认和取消。连续反馈让用户知道系统正在理解他的动作。

界面要服务现实动作

AirTag 查找时,用户的注意力在房间里。屏幕只是辅助。

所以 Apple 使用大箭头、大距离数字、明显颜色变化、声音和触觉。用户可以用余光读到方向,也可以通过声音判断 AirTag 在空间里的位置。进入手臂范围后,触觉反馈比声音更适合引导最后的定位。

这类设计的目标是减少盯屏时间。界面要把注意力还给物理世界。

详细内容

这场 session 是设计导向,没有 Code tab。下面的代码把 transcript 中的设计原则落成可实现的应用状态。API 层面,Resources 指向 Nearby Interaction framework;设计层面,演讲强调距离、方向、视觉、声音、触觉和中断。

1. 按设备能力降级

02:48)Apple 先用分享面板说明空间感知的降级策略。所有设备都可以用 Bluetooth 和 Wi-Fi 查找附近的人。带 U1 的 iPhone 可以进一步优先显示用户正面对着或距离很近的人。

import Foundation

struct ShareCandidate {
    let name: String
    let communicationScore: Double
    let isNearby: Bool
    let isFacing: Bool
}

enum SpatialAwarenessLevel {
    case bluetoothAndWiFi
    case u1DirectionAware
}

func rankCandidates(
    _ candidates: [ShareCandidate],
    level: SpatialAwarenessLevel
) -> [ShareCandidate] {
    candidates.sorted { first, second in
        score(first, level: level) > score(second, level: level)
    }
}

func score(_ candidate: ShareCandidate, level: SpatialAwarenessLevel) -> Double {
    var result = candidate.communicationScore

    if candidate.isNearby {
        result += 10
    }

    if level == .u1DirectionAware && candidate.isFacing {
        result += 20
    }

    return result
}

let people = [
    ShareCandidate(name: "Arian", communicationScore: 42, isNearby: true, isFacing: false),
    ShareCandidate(name: "Taylor", communicationScore: 30, isNearby: true, isFacing: true),
    ShareCandidate(name: "Linus", communicationScore: 50, isNearby: false, isFacing: false)
]

let ranked = rankCandidates(people, level: .u1DirectionAware)
print(ranked.map(\.name))

关键点:

  • ShareCandidate 保存候选人的通信频率、是否附近、是否正对这些输入。
  • SpatialAwarenessLevel 把设备能力分成基础近场能力和 U1 方向感知能力。
  • rankCandidates 只负责排序,界面可以复用同一组候选人。
  • score 在基础设备上只增加附近权重。
  • level == .u1DirectionAware 时才使用 isFacing
  • 这个结构对应 transcript 中“能力并非所有设备都有,设计要适配不同能力”的要求。

2. 根据距离切换主反馈

04:00)AirTag 查找过程会随距离改变。远处用地图和方向。连接后用箭头。靠近时,朝向判断更严格。进入手臂范围后,触觉成为主反馈。

import Foundation

enum PrimaryFeedback: String {
    case map
    case arrow
    case haptics
}

struct FindingFeedback {
    let primaryFeedback: PrimaryFeedback
    let facingToleranceInDegrees: Double
    let showsDistance: Bool
}

func feedbackForFindingItem(distanceInMeters: Double) -> FindingFeedback {
    if distanceInMeters > 10 {
        return FindingFeedback(
            primaryFeedback: .map,
            facingToleranceInDegrees: 90,
            showsDistance: true
        )
    }

    if distanceInMeters > 1 {
        return FindingFeedback(
            primaryFeedback: .arrow,
            facingToleranceInDegrees: 35,
            showsDistance: true
        )
    }

    return FindingFeedback(
        primaryFeedback: .haptics,
        facingToleranceInDegrees: 15,
        showsDistance: false
    )
}

let far = feedbackForFindingItem(distanceInMeters: 12)
let near = feedbackForFindingItem(distanceInMeters: 0.6)

print(far.primaryFeedback.rawValue)
print(near.primaryFeedback.rawValue)

关键点:

  • PrimaryFeedback 表示当前阶段最重要的反馈形式。
  • distanceInMeters > 10 时返回 .map,对应“大致位置”的阶段。
  • distanceInMeters > 1 时返回 .arrow,对应箭头引导阶段。
  • 最后返回 .haptics,对应手臂范围内的精细查找。
  • facingToleranceInDegrees 随距离缩小,表达演讲中“远处角度更宽容,近处更精确”的设计。
  • showsDistance 在最后阶段关闭,避免屏幕数字抢走对触觉的注意力。

3. 用连续反馈表达确认和取消

07:23)HomePod mini 的传输体验使用两个距离边界。到达第一个边界时开始反馈。继续靠近会进入确认区。拉远可以自然取消。

import Foundation

enum TransferPhase: String {
    case idle
    case ready
    case confirming
    case completed
}

struct TransferFeedback {
    let phase: TransferPhase
    let bannerScale: Double
    let backgroundBlur: Double
    let hapticIntensity: Double
}

func transferFeedback(distanceToSpeaker: Double) -> TransferFeedback {
    let firstThreshold = 0.6
    let secondThreshold = 0.12

    if distanceToSpeaker > firstThreshold {
        return TransferFeedback(
            phase: .idle,
            bannerScale: 0,
            backgroundBlur: 0,
            hapticIntensity: 0
        )
    }

    if distanceToSpeaker > secondThreshold {
        let progress = (firstThreshold - distanceToSpeaker) / (firstThreshold - secondThreshold)

        return TransferFeedback(
            phase: .ready,
            bannerScale: 0.8 + progress * 0.2,
            backgroundBlur: progress,
            hapticIntensity: progress
        )
    }

    return TransferFeedback(
        phase: .completed,
        bannerScale: 1,
        backgroundBlur: 1,
        hapticIntensity: 1
    )
}

let movingCloser = transferFeedback(distanceToSpeaker: 0.3)
let reachedSpeaker = transferFeedback(distanceToSpeaker: 0.08)

print(movingCloser.phase.rawValue)
print(reachedSpeaker.phase.rawValue)

关键点:

  • firstThreshold 是开始反馈的边界。
  • secondThreshold 是确认传输的边界。
  • 距离大于第一个边界时,状态是 .idle,界面不抢注意力。
  • 两个边界之间计算 progress,让横幅、模糊和触觉连续变化。
  • 距离小于第二个边界时,状态进入 .completed
  • 用户把 iPhone 拉远后,函数会回到 .idle.ready,这就是自然取消。

4. 为余光阅读设计界面文本

15:23)AirTag 查找时,用户的注意力应该放在周围环境。Apple 使用更大的距离文字、中心箭头、明显颜色变化、声音和触觉,让用户不必长时间盯着屏幕。

import Foundation

enum FindingState {
    case connecting
    case pointing(distanceInMeters: Double, isFacingTarget: Bool)
    case withinReach
    case signalLost
}

struct PeripheralMessage {
    let title: String
    let detail: String
    let usesLargeType: Bool
    let shouldPlaySound: Bool
    let shouldUseHaptics: Bool
}

func message(for state: FindingState) -> PeripheralMessage {
    switch state {
    case .connecting:
        return PeripheralMessage(
            title: "正在连接",
            detail: "移动 iPhone 搜索附近物品",
            usesLargeType: true,
            shouldPlaySound: false,
            shouldUseHaptics: false
        )
    case .pointing(let distance, let isFacingTarget):
        return PeripheralMessage(
            title: String(format: "%.1f 米", distance),
            detail: isFacingTarget ? "继续向前" : "转动 iPhone 寻找方向",
            usesLargeType: true,
            shouldPlaySound: isFacingTarget,
            shouldUseHaptics: isFacingTarget
        )
    case .withinReach:
        return PeripheralMessage(
            title: "就在附近",
            detail: "缓慢移动 iPhone,跟随触觉反馈",
            usesLargeType: true,
            shouldPlaySound: false,
            shouldUseHaptics: true
        )
    case .signalLost:
        return PeripheralMessage(
            title: "信号丢失",
            detail: "回到刚才的位置重新连接",
            usesLargeType: true,
            shouldPlaySound: false,
            shouldUseHaptics: false
        )
    }
}

let state = FindingState.pointing(distanceInMeters: 2.4, isFacingTarget: true)
let currentMessage = message(for: state)
print(currentMessage.title)
print(currentMessage.detail)

关键点:

  • FindingState 覆盖连接、指向、手臂范围内、信号丢失四个状态。
  • PeripheralMessage 把文案、字号策略、声音和触觉放在一起。
  • .pointing 状态显示大号距离,让用户用余光读取。
  • isFacingTarget 为真时才播放声音和触觉,形成稳定的因果关系。
  • .withinReach 关闭声音,使用触觉反馈,对应 transcript 中近距离查找不用声音的说明。
  • .signalLost 给出恢复动作,避免用户在中断后不知道下一步。

5. 让反馈跨感官配合

12:45)Apple 建议把视觉反馈作为连续层,用声音和触觉强调关键时刻。声音和触觉要有清楚、可重复的因果关系。

import Foundation

struct FeedbackCue {
    let visualProgress: Double
    let soundName: String?
    let hapticPattern: String?
}

func cueForArrowAlignment(wasAligned: Bool, isAligned: Bool, distanceInMeters: Double) -> FeedbackCue {
    let visualProgress = max(0, min(1, 1 - distanceInMeters / 5))

    if !wasAligned && isAligned {
        return FeedbackCue(
            visualProgress: visualProgress,
            soundName: "direction-lock",
            hapticPattern: "snap"
        )
    }

    if isAligned {
        return FeedbackCue(
            visualProgress: visualProgress,
            soundName: nil,
            hapticPattern: "pulse"
        )
    }

    return FeedbackCue(
        visualProgress: visualProgress,
        soundName: nil,
        hapticPattern: nil
    )
}

let cue = cueForArrowAlignment(
    wasAligned: false,
    isAligned: true,
    distanceInMeters: 3
)

print(cue.visualProgress)
print(cue.soundName ?? "no sound")
print(cue.hapticPattern ?? "no haptics")

关键点:

  • visualProgress 是持续变化的视觉层,距离越近数值越大。
  • !wasAligned && isAligned 表示刚刚对准目标。
  • 刚对准时返回声音 direction-lock 和触觉 snap,对应演讲中的“箭头归位”时刻。
  • 持续对准时只保留 pulse,避免重复播放声音。
  • 偏离方向时关闭声音和触觉,让反馈和动作保持一一对应。

核心启发

1. 做一个“靠近即切换”的音频控制

  • 做什么:用户拿着 iPhone 靠近某个音箱时,界面自动显示传输提示,继续靠近完成播放切换,拉远取消。
  • 为什么值得做:HomePod mini 的例子证明,距离边界和连续反馈可以替代设备列表。
  • 怎么开始:先把附近设备抽象成 TransferPhase,用距离更新横幅、模糊和触觉强度。硬件层接入 Nearby Interaction 或你已有的近场发现能力。

2. 做一个仓库物品查找工具

  • 做什么:给工具箱、样品盒或仓储标签加支持近场定位的配件,用 iPhone 引导员工找到目标。
  • 为什么值得做:AirTag 的查找流程适合“先到大致区域,再精确定位”的任务。
  • 怎么开始:把流程拆成地图、箭头、触觉三个阶段。远距离显示区域,近距离显示方向,手臂范围内改用触觉扫描。

3. 做一个“面对谁就分享给谁”的团队协作入口

  • 做什么:会议中打开分享面板时,优先显示你面前或附近的同事。
  • 为什么值得做:演讲提到分享面板会结合面对方向、物理距离、通信频率和最近通信记录预测收件人。
  • 怎么开始:先用通信频率和附近状态排序;在支持 U1 的设备上再加入朝向权重。能力不足时保留原有列表。

4. 做一个不用盯屏的导航辅助

  • 做什么:在展馆、医院或校园里,用大号方向提示、声音和触觉引导用户找房间。
  • 为什么值得做:session 强调空间交互要让用户把注意力放回周围环境。
  • 怎么开始:为每个导航状态生成 PeripheralMessage。界面使用大字号和高对比色,关键状态用声音和触觉确认。

5. 做一个可中断的设备配对流程

  • 做什么:用户靠近配件开始配对,继续靠近确认,移开设备取消。
  • 为什么值得做:HomePod mini 的双边界设计让确认和取消都来自自然身体动作。
  • 怎么开始:定义两个距离阈值。第一个阈值启动反馈,第二个阈值执行操作。距离回退时撤销准备状态,并给出清楚反馈。

关联 Session

评论

GitHub Issues · utterances