WWDC Quick Look 💓 By SwiftGGTeam
Enhance your UI animations and transitions

Enhance your UI animations and transitions

元の動画を見る

ハイライト

iOS 18 では Zoom Transition と SwiftUI-UIKit アニメーション連携が追加され、フレームワークをまたぐ連続的なインタラクションがより簡単になりました。

主要内容

滑らかなトランジションアニメーションの実装には通常、大量のコードが必要です。UIKit では CAAnimation を、SwiftUI では withAnimation を書きます。ビュー階層が混在している場合、両方のアニメーション状態を手動で同期する必要もあります。

iOS 18 がこれを解決します。まず Zoom Transition:リストの cell をタップすると、全画面の詳細ページに拡大します。単純な視覚効果ではありません— トランジション全体がインタラクティブです。いつでもつかんでドラッグでき、途中で逆方向に操作することも可能。Navigation Push だけでなく、fullScreenCover や sheet でも使えます。

より根本的な変更は、SwiftUI Animation が UIView と NSView を直接駆動できるようになったこと。UIView.animate は不要で、SwiftUI の Animation 型を渡すだけ。混在ビュー階層では、1 つのアニメーション API ですべてのビューを管理できます。

Session では UIKit における Zoom Transition のライフサイクルコールバックのタイミングも解説。「Push 進行中に戻るキーを押す」ような複雑なシナリオでも、システムは Push をキャンセルせず、即座に完了して Pop に変換します。View Controller の観点では常に Appeared 状態に到達し、すべてのコールバック(viewWillAppear/willAppear/didAppear)が期待どおりの順序で実行されます。

詳細

Zoom Transition の基本

SwiftUI での Zoom Transition には 2 ステップが必要です:

NavigationLink {
    BraceletEditor(bracelet)
        .navigationTransitionStyle(
            .zoom(
                sourceID: bracelet.id,
                in: braceletList
            )
        )
} label: {
    BraceletPreview(bracelet)
}
.matchedTransitionSource(
    id: bracelet.id,
    in: braceletList
)

キーポイント:

  • navigationTransitionStyle(.zoom(...)) で遷移先ビューが zoom トランジションを使うことを宣言(2:10
  • .matchedTransitionSource でソースビューをマーク。両方が同じ idnamespace を共有
  • トランジション中、ソースビューが遷移先に morph し、画面上の同一 UI 要素の連続性を保持

UIKit での使い方も同様。Push 前に preferredTransition を設定:

func showEditor(for bracelet: Bracelet) {
    let braceletEditor = BraceletEditor(bracelet)
    braceletEditor.preferredTransition = .zoom { context in
        let editor = context.zoomedViewController
            as! BraceletEditor
        return cell(for: editor.bracelet)
    }
    navigationController?.pushViewController(braceletEditor, animated: true)
}

キーポイント:

  • preferredTransition は遷移先 View Controller に設定(3:02
  • クロージャは zoom in と zoom out でそれぞれ 1 回呼ばれる
  • ビューを直接キャプチャするのではなく、安定した識別子(モデルオブジェクトなど)を返す。UICollectionView の再利用問題を避けるため
  • context から現在のデータを取得し、詳細ページ内で左右スワイプ後も正しいソースビューに戻れる

トランジションライフサイクルと互換性

中断可能な UIKit トランジションの重要な設計:中断された Push はキャンセルされず、即座に完了して Pop になる

これは次を意味します:

  • Push の viewWillAppearviewDidAppear は必ず呼ばれる
  • 同じ runloop サイクルで Pop が始まり、viewWillDisappearviewDidDisappear がトリガーされる
  • コードの観点では、Push が正常完了した直後に Pop が始まったように見える

既存コードとの互換性のためです。Push をキャンセルすると View Controller が「本当に表示された」ことがなく、「viewWillAppear で 1 回だけ実行」という前提の多くのコードが壊れます。

SwiftUI Animation による UIKit ビューの駆動

iOS 18 の最大の連携は、SwiftUI Animation 型を UIView アニメーションに直接使用できること:

UIView.animate(.spring(duration: 0.5)) {
    bead.center = endOfBracelet
}

キーポイント:

  • パラメータは従来の UIView.AnimationOptions ではなく SwiftUI の Animation 型(8:39
  • CustomAnimations を含むすべての SwiftUI アニメーション型をサポート
  • 内部では CAAnimation を作成せず、layer の presentation values を直接操作
  • presentation layer は引き続きアニメーション状態を正しく反映

UIViewRepresentable では、新しい context.animate でアニメーションを橋渡し:

struct BeadBoxWrapper: UIViewRepresentable {
    @Binding var isOpen: Bool

    func updateUIView(_ box: BeadBox, context: Context) {
        context.animate {
            box.lid.center.y = isOpen ? -100 : 100
        }
    }
}

struct BraceletEditor: View {
    @State private var isBeadBoxOpen = false
    var body: some View {
        BeadBoxWrapper($isBeadBoxOpen.animated())
            .onTapGesture {
                isBeadBoxOpen.toggle()
            }
    }
}

キーポイント:

  • .animated() modifier で binding の変化にアニメーション情報を付与(9:56
  • context.animate は現在の Transaction 上のアニメーションをキャプチャし、UIView の変更に適用
  • Transaction にアニメーションがなければ即座に実行。手動判定不要
  • SwiftUI と UIView のアニメーションが完全に同期

ジェスチャー駆動の連続アニメーション

ドラッグジェスチャー終了後に速度を保持するには、従来は初期 velocity の計算が必要:

switch gesture.state {
case .changed:
    UIView.animate(.interactiveSpring) {
        bead.center = gesture.translation
    }

case .ended:
    UIView.animate(.spring) {
        bead.center = endOfBracelet
    }
}

キーポイント:

  • ジェスチャー中は .interactiveSpring。各変化で新しいアニメーションを作成し、互いに retarget(11:39
  • ジェスチャー終了後は通常の .spring を使い、以前の velocity を自動継承
  • unit velocity の手動計算不要— SwiftUI アニメーションが自動処理
  • SwiftUI アニメーションの利点:連続イベントでトリガーされたアニメーションは自動的にマージし速度を保持

重要ポイント

  1. デフォルト Push アニメーションの代わりに Zoom Transition を使う:「リストの大きな画像から詳細ページへ」のシナリオがあれば、Zoom Transition を直接有効化。iOS 18 のシステムデザイン言語で、Photos や Messages も使用。modifier と source マーカーを追加するだけで、視覚的連続性が大幅に向上。

  2. 混在ビュー階層では SwiftUI Animation でモーションを統一:UIKit 部分で UIView.animate を手書きし、SwiftUI 部分で withAnimation するのではなく。新 API で 1 つのアニメーション型ですべてのビューを駆動。特に UIViewRepresentable 内の UIKit ビューは context.animate で橋渡し— 外部のアニメーション設定が自動的に伝播。

  3. ドラッグインタラクションでは interactiveSpring で速度計算を簡略化:「ドラッグして戻る」シナリオ(プルリフレッシュ、カードドラッグ)では、ジェスチャー変化に .interactiveSpring を使い、ジェスチャー終了時の通常 spring アニメーションが速度を自動継承。velocity/distance 比の手動計算不要— コードが簡潔で、感触も自然。

関連セッション

コメント

GitHub Issues · utterances