iOSにおけるHeroライブラリを活用したインタラクティブトランジションの実装手法

ライブラリの概要と設計思想

iOSおよびtvOS環境で動作する「Hero」は、ビュートランジションを宣言的に記述できるオープンソースライブラリです。従来のCore AnimationやUIViewアニメーションと比較し、遷移前後のビューに識別子を付与するだけで補間パスを自動計算する仕組みを採用しています。特に、ユーザーの操作入力をアニメーションの進行度と直結させるインタラクティブトランジションを実装する際に、状態管理の複雑さを大幅に抑えられます。

アーキテクチャと主要な構成要素

Heroのトランジション処理は、主に以下の3つの概念に基づいて構築されています。

  • HeroModifier: 透明度、スケール、回転角、移動ベクトルなどのアニメーション属性を定義する構造体です。複数のModifierを配列で結合することで、初期状態と終了状態の差分を合成できます。
  • トランジションコンテキスト: アニメーションのライフサイクル中に生成されるオブジェクトで、現在の進行率(0.0〜1.0)や遷移前後のビュー参照を保持します。リアルタイムのプロパティ補間や条件分岐に利用します。
  • インタラクティブ制御インターフェース: `UIPanGestureRecognizer`や`UIRotationGestureRecognizer`などのUIKitジェスチャ認識器と連携し、`update(progress:)`メソッド経由でアニメーションの進捗を直接操作できます。

環境構築と基礎設定

プロジェクトへの導入はSwift Package Managerが推奨されます。パッケージ依存性ファイルにリポジトリURLを追加し、ターゲットにリンクした後にビルドを実行します。基本となるビューのタグ付けは`hero.id`プロパティで完了します。遷移元と遷移先のビューに同一の文字列を割り当てることで、Heroは自動的に座標変換と補間を処理します。

// 遷移元のViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let targetVC = segue.destination as? DetailViewController else { return }
    targetVC.hero.isEnabled = true
    sharedContent.hero.id = "content_panel"
    targetVC.targetContainer.hero.id = "content_panel"
}

ジェスチャ連携によるインタラクティブトランジション

ナビゲーションスタックにおける標準的なバック操作(右スワイプ)をHeroのアニメーションに統合するには、フラグ設定のみで有効化可能です。`hero.interactivePop`を`true`に設定することで、システム標準の操作感を維持したままHeroが管理するトランジションコンテキストと連動します。

override func viewDidLoad() {
    super.viewDidLoad()
    hero.isEnabled = true
    // ナビゲーション制御との連動を有効化
    hero.interactivePop = true
}

カスタムジェスチャの高度な制御

標準のスワイプ以外の操作、例えば回転やピンチに合わせてビューを変形させるケースでは、カスタムジェスチャ認識器とHeroのAPIを直接結合する必要があります。以下の実装では、回転ジェスチャの角度を進行度へ正規化し、閾値を超えた時点で遷移を確定または破棄するロジックを構築しています。

private var rotationRecognizer: UIRotationGestureRecognizer!

override func viewDidLoad() {
    super.viewDidLoad()
    rotationRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(processRotation(_:)))
    view.addGestureRecognizer(rotationRecognizer)
}

@objc private func processRotation(_ recognizer: UIRotationGestureRecognizer) {
    // 回転角度を0.0〜1.0の範囲に正規化
    let normalizedProgress = abs(recognizer.rotation) / (.pi * 2)
    hero.update(progress: normalizedProgress)

    if recognizer.state == .ended {
        // 一定以上の回転が検出された場合のみ遷移を完了
        if abs(recognizer.rotation) > (.pi / 3) {
            hero.finish()
        } else {
            hero.cancel()
        }
    }
}

複数の修飾子を組み合わせた表現

単一の属性だけでなく、複数の幾何学的変換を同時に変更したい場合は、`heroModifiers`配列に任意の数だけ要素を追加します。Heroは内部でこれらの値を適切に補間し、合成行列として適用します。

// 初期状態とターゲット状態をModifierで定義
sourceView.heroModifiers = [.opacity(1.0), .scale(1.0), .translate(x: 0, y: 0)]
destinationView.heroModifiers = [.opacity(0.2), .scale(0.6), .rotate(degrees: -15)]

実務での適用ケース

ギャラリーアプリのシームレスなズーム遷移

一覧表示のサムネイルをタップした際、詳細画面のフルサイズイメージへ滑らかに移行するには、両者のビューに同一の`hero.id`を付与し、スケーリング修飾子を追加するだけで実現できます。Heroは画面中央の座標変換とズームインを自動計算し、メモリ効率的に描画します。

// サムネイルセル
imageView.hero.id = "photo_\(item.identifier)"
imageView.heroModifiers = [.opacity(1.0)]

// 詳細ビュー
fullScreenImage.hero.id = "photo_\(item.identifier)"
fullScreenImage.heroModifiers = [.opacity(1.0)]

ECプラットフォームのカート追加アニメーション

商品リストからカートアイコンへアイテムが飛ぶような表現は、`heroModifiers`の初期値と終了値を制御し、カスタムパスで追跡させることで構築可能です。遷移中に要素の描画順序(z-index)を調整する際は、`hero.zPosition`プロパティを活用し、重なり順を明示的に指定します。

productCardView.heroModifiers = [
    .scale(1.0),
    .opacity(1.0),
    .zPosition(1)
]
cartBadgeView.heroModifiers = [
    .scale(0.5),
    .opacity(0.8),
    .zPosition(2)
]

タグ: Hero iOS tvOS Swift UIKit

6月22日 17:48 投稿