Create custom hover effects in visionOS
Spatial Computing 进阶 20m

在 visionOS 中创建自定义悬停效果

Create custom hover effects in visionOS

2024年6月10日

在 Apple 官方观看视频

一句话判断

visionOS 2 的 Custom Hover Effect API 让你能精确控制”用户看向某个视图时”的视觉反馈,从缩放到裁剪到透明度,配合 Effect Group 和延迟效果,这是做 visionOS 精品交互不可或缺的一套 API。

这场 Session 讲了什么

在 visionOS 上,当用户看向交互元素时,系统会自动应用高亮效果。这些 Hover Effect 让 app 感觉有响应性,同时提供反馈告诉用户哪个元素会被触发。标准高亮效果对大多数场景够用了,但有些视图需要自定义效果——Slider 显示旋钮来引导交互、返回按钮展开显示上一页名称、Tab Bar 弹出显示标签、Safari 导航栏展开显示标签页。

visionOS 2 引入了全新的 Custom Hover Effect API。这套 API 从设计之初就考虑了隐私保护——效果由系统在 app 进程之外应用,不需要额外的 entitlement 或 extension。自定义效果可以应用在 SwiftUI 视图的任何位置,包括 ornaments 和 RealityView 的 attachment。触发方式也不只是眼动追踪,还包括手指靠近和鼠标悬停。

API 的核心概念分为四层:Content Effect(改变视图外观的基础效果,包括缩放、裁剪、透明度)、Effect Group(让多个效果协同触发)、Delayed Effect(控制效果的触发时机)、以及 CustomHoverEffect 协议(创建可复用的、尊重无障碍偏好的自定义效果)。

值得深挖的点

Effect Group 解决了空间交互中的”触发区域”问题

在 visionOS 中,每个 Hover Effect 绑定到它附加的视图上。如果你给一个按钮整体加了缩放效果,给按钮里的文字单独加了透明度效果,你会遇到一个问题:缩放在你看向按钮任何位置时都会触发,但透明度只有你看向文字区域时才生效。结果就是,当你看向按钮的图标部分时,按钮变大了但文字没出现——交互体验是割裂的。

Effect Group 的设计优雅地解决了这个问题。同一个 Group 内的效果会同步激活——只要用户看向 Group 内任何一个视图,所有效果都会一起触发。而且系统提供了两种分组方式:显式分组(创建 HoverEffectGroup 并手动添加每个效果)和隐式分组(在视图上添加 hoverEffectGroup modifier,自动将该视图及其子视图的所有效果归入同一组)。

隐式分组特别实用,大多数情况下你甚至不需要提供 Group ID——系统会自动为你创建。这种”默认帮你做好,需要时再精确控制”的设计哲学贯穿了整个 API。

Content Effect 的组合式设计思路

Content Effect 只能改变视图的外观,不能影响周围视图的布局。这个限制看似严格,实则非常聪明。在空间计算场景中,如果 Hover Effect 能改变布局,就会导致视图位置跳变,而用户的眼球追踪是基于位置的——布局变化会导致追踪目标偏移,形成恶性循环。

通过限制为纯视觉效果(缩放、裁剪、透明度),API 确保了交互的稳定性。而组合这些基础效果,你依然能实现复杂的视觉表现。Session 中的展开按钮效果就是三个 Content Effect 的组合:缩放让按钮变大、裁剪从圆形扩展为完整矩形、透明度让文字从隐藏到显示。

代码片段

基础缩放效果——按钮在注视时放大

struct ProfileButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            // 标准高亮效果
            .hoverEffect()
            // 自定义缩放效果
            .hoverEffect { isActive, proxy in
                // isActive 为 true 时应用激活态
                // 系统会自动处理状态间的过渡动画
                let scale: CGFloat = isActive ? 1.05 : 1.0
                AnyView(configuration.label.scaleEffect(scale))
            }
    }
}

场景:让按钮在用户注视时轻微放大,增强交互引导感。坑点:缩放比例不宜过大,超过 1.1 会显得突兀,0.03-0.05 的范围比较自然。

裁剪效果——按钮从圆形展开为完整形状

.hoverEffect { isActive, proxy in
    let clipWidth: CGFloat = isActive 
        ? proxy.size.width    // 激活态:完整宽度
        : proxy.size.height   // 非激活态:宽高相等,形成圆形
    
    AnyView(
        configuration.label
            .clipShape(
                Capsule()
                    .size(width: clipWidth, height: proxy.size.height),
                anchor: .leading  // 从左侧对齐,确保图标可见
            )
    )
}

场景:个人资料按钮在未注视时只显示图标(圆形),注视时展开显示详细信息。坑点:一定要设置 anchor 参数,否则裁剪可能从错误的方向展开。

隐式 Effect Group——让多个效果同步触发

// 方式一:隐式分组(推荐,大多数场景够用)
VStack {
    // 按钮整体有缩放和裁剪效果
    ProfileButton(/* ... */)
        .hoverEffect { /* 缩放+裁剪效果 */ }
    
    // 文字有透明度效果
    Text("用户名")
        .hoverEffect { isActive, _ in
            AnyView(Text("用户名").opacity(isActive ? 1 : 0))
        }
}
.hoverEffectGroup()  // 自动将所有子效果归入同一组

// 方式二:显式分组(需要精确控制时使用)
@Namespace private var profileGroup
let group = HoverEffectGroup(id: profileGroup)

ProfileButton(/* ... */)
    .hoverEffect(.highlight, group: group)
Text("用户名")
    .hoverEffect { /* 透明度效果 */ }
    .hoverEffect(group: group)  // 手动指定加入哪个组

场景:确保按钮展开和文字淡入同步发生。坑点:显式分组需要用 @Namespace 创建唯一 ID,同一个 Namespace 不能用在不同的 Group 上。

最佳实践

  • 隐私由系统保证:自定义 Hover Effect 由系统在 app 进程外应用,你不需要申请任何额外权限。不要试图绕过这个机制去获取用户的注视方向信息。
  • 尊重无障碍偏好:通过 CustomHoverEffect 协议创建的效果可以读取用户的辅助功能设置,据此调整效果的强度或禁用某些动画。
  • 效果幅度要克制:空间计算中的视觉效果离用户眼睛很近,过大的缩放或闪烁会让人不适。缩放控制在 5% 以内,透明度变化平滑过渡。
  • 优先使用隐式分组:只有在需要跨视图层次分组或需要多个独立组时才用显式分组。
  • Ornament 和 RealityView 也能用:Custom Hover Effect 不限于常规 SwiftUI 视图,空间计算中的 3D 内容和装饰元素同样适用。

还有什么值得关注

  • RealityKit 也有新的 API 来给 3D 内容添加 Hover Effect,详见《What’s New in RealityKit》Session。
  • Delayed Effect 可以让某些效果延迟触发,适合需要分层引导用户注意力的场景。
  • hoverEffect block 中的 isActive 参数由系统在效果创建时就确定,不是在用户实际注视时才调用——这意味着你不能在里面放有副作用的代码。
WWDC 2024