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

Create custom hover effects in visionOS

元の動画を見る

ハイライト

visionOS 2 では Custom Hover Effect API が導入され、ユーザーが注視したり、指を近づけたり、マウスをホバーしたりするとビューが反応します。効果はプライバシー保護のためアプリプロセス外でシステムが適用します。


主要内容

visionOS では、ユーザーがインタラクティブ要素を見ると、システムが自動的にハイライト効果を適用します。これらの Hover Effect はアプリの応答性を高め、どの要素がトリガーされるかを示します。標準ハイライトは多くのシーンで十分ですが、カスタム効果が必要なビューもあります——スライダーでノブを表示して操作を誘導、戻るボタンが展開して前ページ名を表示、Tab Bar がラベルをポップアップ、Safari のナビゲーションバーがタブを展開、など。

visionOS 2 の Custom Hover Effect API は最初からプライバシーを考慮して設計されています。効果はシステムがアプリプロセス外で適用するため、追加の entitlement や extension は不要です。カスタム効果は ornaments や RealityView の attachment を含め、SwiftUI ビューの任意の位置に適用できます。トリガーは視線追跡だけでなく、指の接近やマウスホバーも含みます。

API の核心概念は 4 層に分かれます。Content Effect(スケール、クリップ、透明度など見た目の基本変化)、Effect Group(複数効果の協調トリガー)、Delayed Effect(トリガータイミングの制御)、CustomHoverEffect プロトコル(アクセシビリティ設定を尊重する再利用可能なカスタム効果)。


詳細

Content Effect:基本の見た目変化

Content Effect はビューの見た目を変える基本効果で、スケール(scaleEffect)、クリップ(clipShape)、透明度(opacity)の 3 種類をサポートします。レイアウトには影響せず、視覚的な表示だけを変えます。

最もシンプルなのはスケール効果です。ButtonStyle で hoverEffect modifier を使います:

// 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 の 2 回呼ばれ、アクティブ/非アクティブ状態を定義する
  • システムは注視時ではなく事前にこれらのブロックを呼び出して効果を生成する
  • 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 を使えばすべての効果が一緒にアクティブになります。2 つの方法があります:

明示的グループ化(最も柔軟):

// 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 effect は即座にトリガーされますが、UI を閲覧中は邪魔になり得ます。適切な遅延でちらつきを防げます。

追加コンテンツを表示する効果(展開ボタンなど)はより長い遅延が必要です。即時フィードバック(スケールなど)は即座に応答すべきです。

// 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(...)
        }
    }
}

キーポイント:

  • CustomHoverEffectbody(content:configuration:) の実装が必要
  • accessibilityReduceMotion 環境変数で「動きを減らす」が有効か判定
  • 動きに敏感なユーザー向けの代替効果を用意する

アニメーションサポート

Hover Effect は linear、easeOut、spring、カスタム timing curve など一般的な SwiftUI アニメーションをサポートします。CustomAnimation 型はアプリプロセス外では適用できないため非対応です。


重要ポイント

1. 表示型コンテンツに遅延展開効果を追加

空間コンピューティング UI では、追加コンテンツはデフォルトで非表示、注視時に展開します。視覚ノイズを減らしつつ情報へのアクセスを保てます。向いているシーン:設定パネルの詳細オプション、リスト項目の拡張情報、ナビゲーションメニューの子項目。

2. コンテンツ種別に応じた遅延戦略を選ぶ

即時フィードバック系(スケール、ハイライト)は即座にトリガーし、要素が操作可能であることを伝える。表示型(展開、フェードイン)は 200〜300ms の遅延を設け、閲覧中の頻繁なトリガーを避ける。

3. 動きに敏感なユーザー向けの代替を用意

accessibilityReduceMotion を確認し、動的効果を無効化または簡略化する。展開/収縮の代わりにクロスフェードを使うか、効果を除去して静的ハイライトのみ残す。動きを伴う UI はすべてこれを行うべき。


関連セッション

コメント

GitHub Issues · utterances