WWDC Quick Look 💓 By SwiftGGTeam
Create custom hover effects in visionOS

Create custom hover effects in visionOS

观看原视频

Highlight

visionOS 2 引入 Custom Hover Effect API,让视图在用户注视、手指靠近或鼠标悬停时响应变化,系统在 app 进程外应用效果以保护隐私。


核心内容

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

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

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


详细内容

Content Effect:基础外观变化

Content Effect 是改变视图外观的基础效果,支持三种类型:缩放(scaleEffect)、裁剪(clipShape)和透明度(opacity)。这些效果只改变视图的视觉呈现,不影响布局。

最简单的是缩放效果。在 ButtonStyle 中使用 hoverEffect 修饰符:

// Button with Scale Effect([04:06](https://developer.apple.com/videos/play/wwdc2024/10152/?time=246))
ButtonStyle {
    configuration
        .hoverEffect(.highlight) // 标准高亮
        .hoverEffect(isActive: isActive in
            configuration.label
                .scaleEffect(isActive ? 1.05 : 1.0) // 激活时放大 5%
        )
}

关键点:

  • hoverEffect 块会被调用两次,isActive 分别为 true 和 false,用于定义激活和未激活状态
  • 系统会预先调用这些块生成效果,而非在用户注视时实时调用
  • 5% 的缩放是经过验证的舒适幅度

Clip Effect 用于隐藏和揭示内容。通过改变 clipShape 的大小来控制可见区域:

// Button with Clip and Scale Effects([05:37](https://developer.apple.com/videos/play/wwdc2024/10152/?time=337))
ButtonStyle {
    configuration
        .hoverEffect(.highlight)
        .hoverEffect(isActive: isActive, proxy: proxy in
            configuration.label
                .clipShape(
                    Capsule()
                        .size(
                            width: isActive ? proxy.size.width : proxy.size.height,
                            height: proxy.size.height
                        )
                        .anchor(isActive ? .center : .leading) // 非激活时对齐左侧,只显示图标
                )
                .scaleEffect(isActive ? 1.05 : 1.0)
        )
}

关键点:

  • proxy 参数提供视图的几何信息
  • 激活时 clipShape 跨越按钮全宽,非激活时变成正方形只显示图标
  • anchor 参数控制 clipShape 的对齐方式

Effect Group:协同多个效果

当多个效果需要协同触发时,需要使用 Effect Group。默认情况下,效果只在用户注视该视图时激活。如果把缩放和裁剪应用到整个按钮,而把透明度应用到详情文字,就会出现问题——只有注视文字时才会淡入。

解决方案是使用 Effect Group,让所有效果一起激活。有两种方式:

显式分组(最灵活的控制):

// Expanding Button with Explicit Group([08:19](https://developer.apple.com/videos/play/wwdc2024/10152/?time=499))
@Namespace private var profileButtonGroup
@State private var group = HoverEffectGroup()

var body: some View {
    Button { ... } label: {
        Label("Profile", systemImage: "person.circle")
    }
    .buttonStyle(ProfileButtonStyle(group: group))
}

struct ProfileButtonStyle: ButtonStyle {
    var group: HoverEffectGroup
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .hoverEffect(.highlight)
            .hoverEffect(isActive: ..., in: group) // 将效果加入组
    }
}

隐式分组(最便捷):

// Expanding Button with Implicit Group([09:13](https://developer.apple.com/videos/play/wwdc2024/10152/?time=553))
Button { ... } label: {
    Label("Profile", systemImage: "person.circle")
}
.hoverEffectGroup() // 自动将所有子视图的效果加入组

关键点:

  • 显式分组需要创建 HoverEffectGroup 并用 Namespace 标识
  • 隐式分组只需在父视图添加 hoverEffectGroup() 修饰符
  • 组内任一效果被触发时,整个组的效果都会激活

Delayed Effect:控制触发时机

默认情况下,hover effects 立即触发。但这会造成干扰——用户浏览界面时会频繁激活效果。适当延迟可以避免闪烁。

对于揭示额外内容的效果(如展开按钮),需要更长的延迟。对于提供即时反馈的效果(如缩放),应该立即响应。

// Expanding Button with Delay([10:53](https://developer.apple.com/videos/play/wwdc2024/10152/?time=653))
.hoverEffect(isActive: isActive in
    configuration.label
        .scaleEffect(isActive ? 1.05 : 1.0) // 缩放立即响应
        .clipShape(...)
        .opacity(isActive ? 1 : 0)
        .animation(
            isActive ? .default.delay(0.3) : .default.delay(0.1),
            value: isActive
        )
)

关键点:

  • 激活时延迟更长(0.3 秒),非激活时延迟更短(0.1 秒)
  • 缩放效果不需要延迟,因为它提供即时反馈
  • 延迟值需要在真机上测试,找到舒适的感觉

CustomHoverEffect 协议:尊重无障碍偏好

对于有运动敏感性的用户,动态效果可能让人不适。通过 CustomHoverEffect 协议,可以根据用户的辅助功能设置调整效果。

// Cross-fade effect for reduced motion([12:37](https://developer.apple.com/videos/play/wwdc2024/10152/?time=757))
struct AccessibilityHoverEffect: CustomHoverEffect {
    func body(content: Content, configuration: Configuration) -> some View {
        @Environment(\.accessibilityReduceMotion) var reduceMotion
        
        if reduceMotion {
            // 使用淡入淡出效果
            content.opacity(configuration.isActive ? 1 : 0)
        } else {
            // 使用完整展开效果
            content
                .scaleEffect(configuration.isActive ? 1.05 : 1.0)
                .clipShape(...)
        }
    }
}

关键点:

  • CustomHoverEffect 协议要求实现 body(content:configuration:) 方法
  • accessibilityReduceMotion 环境变量指示用户是否启用了减弱动态效果
  • 应为运动敏感用户提供替代效果

动画支持

Hover Effects 支持常见的 SwiftUI 动画:linear、easeOut、spring、以及自定义 timing curves。但 CustomAnimation 类型不支持,因为它们无法在 app 进程外应用。


核心启发

1. 为揭示性内容添加延迟展开效果

在空间计算界面中,额外内容默认隐藏,注视时才展开。这既减少视觉噪音,又保持信息可访问。适合场景:设置面板的详细选项、列表项的扩展信息、导航菜单的子项。

2. 根据内容类型选择延迟策略

即时反馈类效果(缩放、高亮)应该立即触发,让用户知道元素可交互。揭示性内容类效果(展开、淡入)需要 200-300ms 延迟,避免用户浏览时频繁触发。

3. 为运动敏感用户准备替代方案

检测 accessibilityReduceMotion 偏好,禁用或简化动态效果。可以用淡入淡出替代展开/收缩,或完全移除效果只保留静态高亮。所有涉及运动的 UI 都应该这样做。


关联 Session

评论

GitHub Issues · utterances