mcpField
A
axiom-swiftui-animation-ref

axiom-swiftui-animation-ref

github.com/charleswiltgenデザイン・UI1.2k212 回インストール今日更新
リポジトリ

axiom-swiftui-animation-ref について

SwiftUI のアニメーションを学び、実装する開発者向けのスキルです。補間、アニメーションの種類、トランザクションに加え、iOS 26 の `@Animatable` マクロやカスタム実装の性能指針も扱います。

主な機能

  1. 01Animatable と VectorArithmetic の基礎
  2. 02iOS 26 の `@Animatable` マクロ
  3. 03タイミングカーブとスプリングアニメーション
  4. 04スコープ付きアニメーションとトランザクション
  5. 05カスタムアニメーションアルゴリズム
  6. 06カスタム適合の性能に関する指針

活用シーン

  1. 01カスタムシェイプやレイアウトをアニメーション化
  2. 02スプリングとタイミングカーブの挙動を選択
  3. 03特定のビュー属性だけにアニメーションを適用
  4. 04数値や進捗表示をアニメーション化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-animation-ref

インストール後、Claude Code で/axiom-swiftui-animation-refを入力して読み込まれたか確認します。

詳細

SwiftUI アニメーション

概要

SwiftUI のアニメーションシステムを、基礎概念から高度なテクニックまで網羅したガイドです。このスキルでは、Animatable プロトコル、iOS 26 の @Animatable マクロ、アニメーションの種類、Transaction システムについて説明します。

基本原則 SwiftUI のアニメーションは、VectorArithmetic プロトコルを基盤とする、時間経過に伴う数学的な補間です。この基盤を理解すれば、SwiftUI の宣言型アニメーションシステムの力を最大限に活用できます。

システム要件

  • iOS 13 以降:Animatable プロトコル、タイミングアニメーション/スプリングアニメーション
  • iOS 17 以降:デフォルトのスプリングアニメーション、スコープ付きアニメーション、PhaseAnimator、KeyframeAnimator
  • iOS 18 以降:ズームトランジション、UIKit/AppKit アニメーションのブリッジ
  • iOS 26 以降:@Animatable マクロ

パート 1:アニメーションを理解する

補間とは

アニメーションとは、開始状態と終了状態の間に中間値を生成する処理です。

例:不透明度のアニメーション
.opacity(0) → .opacity(1)

このアニメーションの実行中、SwiftUI は中間値を計算します。

0.0 → 0.02 → 0.05 → 0.1 → 0.25 → 0.4 → 0.6 → 0.8 → 1.0

値の分布を決めるもの

  • アニメーションのタイミングカーブまたは速度関数によって決まります
  • スプリングアニメーションでは物理シミュレーションを使用します
  • タイミングカーブではベジェ曲線を使用します
  • アニメーションの種類ごとに値の計算方法が異なります
VectorArithmetic プロトコル

SwiftUI でアニメーション化するデータは、減算、スケーリング、加算、ゼロ値を提供する VectorArithmetic に準拠する必要があります。これにより、SwiftUI は任意の 2 つの値の間を補間できます。

準拠する組み込み型:CGFloat、Double、Float、Angle(1 次元)、CGPoint、CGSize(2 次元)、CGRect(4 次元)。

重要な点 ベクトル演算により、次元数を抽象化できます。SwiftUI は、単一の汎用実装ですべての型をアニメーション化します。

Int をアニメーション化できない理由

Int は VectorArithmetic に準拠していないため、3 と 4 の間に小数の中間値はありません。SwiftUI は値をただちに切り替えます。

解決策:Float/Double を使い、表示時に Int として扱います。

@State private var count: Float = 0
// ...
Text("\(Int(count))")
    .animation(.spring, value: count)
モデル値と表示値

アニメーション可能な属性には、概念上 2 つの値があります。

モデル値
  • コードで設定される目標値
  • 状態が変化するとすぐに更新されます
  • ビューの body に記述する値です
表示値
  • 現在補間され、描画されている値
  • アニメーション中にフレームごとに更新されます
  • ユーザーに実際に見える値です

例

.scaleEffect(selected ? 1.5 : 1.0)

selected が true になると:

  • モデル値:ただちに 1.5 になります
  • 表示値:時間とともに 1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5 と補間されます

パート 2:Animatable プロトコル

概要

Animatable プロトコルを使うと、補間するデータを定義して、ビューのプロパティをアニメーション化できます。

protocol Animatable {
    associatedtype AnimatableData: VectorArithmetic

    var animatableData: AnimatableData { get set }
}

SwiftUI は、このプロトコルに準拠するすべてのビューに対して、アニメーション可能な属性を構築します。

アニメーション可能な組み込みビュー

多くの SwiftUI モディファイアは Animatable に準拠しています。

視覚効果
  • .scaleEffect() — スケール変換をアニメーション化します
  • .rotationEffect() — 回転をアニメーション化します
  • .offset() — 位置のオフセットをアニメーション化します
  • .opacity() — 不透明度をアニメーション化します
  • .blur() — ぼかしの半径をアニメーション化します
  • .shadow() — 影のプロパティをアニメーション化します
すべての Shape 型
  • Circle、Rectangle、RoundedRectangle
  • Capsule、Ellipse、Path
  • カスタム Shape の実装
多次元データに使う AnimatablePair

複数のプロパティをアニメーション化する場合は、AnimatablePair を使ってベクトルを組み合わせます。たとえば、scaleEffect は CGSize(2 次元)と UnitPoint(2 次元)を組み合わせ、AnimatablePair<CGSize.AnimatableData, UnitPoint.AnimatableData> によって 4 次元ベクトルを作成します。.first と .second を使って各要素にアクセスできます。@Animatable マクロ(iOS 26 以降)を使えば、この定型コードをすべて不要にできます。

カスタム Animatable 準拠
使用する場面
  • カスタムレイアウト(RadialLayout など)をアニメーション化する場合
  • カスタム描画コードをアニメーション化する場合
  • シェイプのパスに影響するプロパティをアニメーション化する場合
例:アニメーションする数値ビュー
struct AnimatableNumberView: View, Animatable {
    var number: Double

    var animatableData: Double {
        get { number }
        set { number = newValue }
    }

    var body: some View {
        Text("\(Int(number))")
            .font(.largeTitle)
    }
}

// Usage
AnimatableNumberView(number: value)
    .animation(.spring, value: value)

仕組み

  1. number が 0 から 100 に変化します
  2. SwiftUI はアニメーションの各フレームで body を呼び出します
  3. 各フレームで新しい number の値を取得します:0 → 5 → 15 → 30 → 55 → 80 → 100
  4. テキストが更新され、補間された整数が表示されます
パフォーマンスに関する警告

カスタムの Animatable 準拠は負荷が高くなります — SwiftUI はメインスレッドで各フレームごとに body を呼び出します。組み込みのエフェクト(.scaleEffect()、.opacity())はメインスレッド以外で実行され、body を呼び出しません。組み込みのモディファイアでは実現できないエフェクト(たとえば、各フレームでサブビューの位置を変更するカスタム Layout のアニメーション)に限り、カスタム準拠を使用してください。


パート 3:@Animatable マクロ(iOS 26 以降)

概要

@Animatable マクロを使うと、Animatable プロトコルに手動で準拠させるための定型コードをなくせます。

iOS 26 より前は、次の作業が必要でした。

  1. Animatable に手動で準拠させる
  2. animatableData のゲッターとセッターを記述する
  3. 複数のプロパティには AnimatablePair を使用する
  4. アニメーション対象外のプロパティを手動で除外する

iOS 26 以降は、@Animatable を追加するだけです。

@MainActor
@Animatable
struct MyView: View {
    var scale: CGFloat
    var opacity: Double

    var body: some View {
        // ...
    }
}

このマクロは次の処理を自動で行います。

  • Animatable への準拠を生成する
  • すべての保存プロパティを調べる
  • VectorArithmetic に準拠するプロパティから animatableData を作成する
  • AnimatablePair で多次元データを処理する
使用前と使用後の比較
@Animatable マクロを使う前
struct HikingRouteShape: Shape {
    var startPoint: CGPoint
    var endPoint: CGPoint
    var elevation: Double
    var drawingDirection: Bool // Don't want to animate this

    // Tedious manual animatableData declaration
    var animatableData: AnimatablePair<AnimatablePair<CGFloat, CGFloat>,
                        AnimatablePair<Double, AnimatablePair<CGFloat, CGFloat>>> {
        get {
            AnimatablePair(
                AnimatablePair(startPoint.x, startPoint.y),
                AnimatablePair(elevation, AnimatablePair(endPoint.x, endPoint.y))
            )
        }
        set {
            startPoint = CGPoint(x: newValue.first.first, y: newValue.first.second)
            elevation = newValue.second.first
            endPoint = CGPoint(x: newValue.second.second.first, y: newValue.second.second.second)
        }
    }

    func path(in rect: CGRect) -> Path {
        // Drawing code
    }
}
@Animatable マクロを使った後
@Animatable
struct HikingRouteShape: Shape {
    var startPoint: CGPoint
    var endPoint: CGPoint
    var elevation: Double

    @AnimatableIgnored
    var drawingDirection: Bool // Excluded from animation

    func path(in rect: CGRect) -> Path {
        // Drawing code
    }
}

コード行数:20 → 12(40%削減)

@AnimatableIgnored

プロパティをアニメーション対象から除外するには、@AnimatableIgnored を使用します。

使用する場面
  • デバッグ値 — 開発時のみ使用するフラグ
  • ID — アニメーションさせるべきではない識別子
  • タイムスタンプ — ビューの作成日時や更新日時
  • 内部状態 — 表示に関係しない管理用の値
  • VectorArithmetic 型ではない型 — 色、文字列、ブール値
例
@MainActor
@Animatable
struct ProgressView: View {
    var progress: Double // Animated
    var totalItems: Int // Animated (if Float, not if Int)

    @AnimatableIgnored
    var title: String // Not animated

    @AnimatableIgnored
    var startTime: Date // Not animated

    @AnimatableIgnored
    var debugEnabled: Bool // Not animated

    var body: some View {
        VStack {
            Text(title)
            ProgressBar(value: progress)
            if debugEnabled {
                Text("Started: \(startTime.formatted())")
            }
        }
    }
}
実際のユースケース

@Animatable は、株価、心拍数、スコア、タイマー、プログレスバーなど、あらゆる数値表示に使用できます。

@MainActor
@Animatable
struct AnimatedValueView: View {
    var value: Double
    var changePercent: Double

    @AnimatableIgnored
    var label: String

    var body: some View {
        VStack(alignment: .trailing) {
            Text("\(value, format: .number.precision(.fractionLength(2)))")
                .font(.title)
            Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent)")
                .foregroundStyle(changePercent > 0 ? .green : .red)
        }
    }
}

// Usage
AnimatedValueView(value: currentPrice, changePercent: 0.025, label: "Price")
    .animation(.spring(duration: 0.8), value: currentPrice)

パート 4:アニメーションの種類

タイミングカーブアニメーション

タイミングカーブアニメーションはベジェ曲線を使用して、時間経過に伴うアニメーションの速度を制御します。

組み込みのプリセット
.animation(.linear)          // Constant speed
.animation(.easeIn)          // Starts slow, ends fast
.animation(.easeOut)         // Starts fast, ends slow
.animation(.easeInOut)       // Slow start and end, fast middle
カスタムタイミングカーブ
let customCurve = UnitCurve(
    startControlPoint: CGPoint(x: 0.2, y: 0),
    endControlPoint: CGPoint(x: 0.8, y: 1)
)

.animation(.timingCurve(customCurve, duration: 0.5))
期間

すべてのタイミングカーブアニメーションでは、期間を任意で指定できます。

.animation(.easeInOut(duration: 0.3))
.animation(.linear(duration: 1.0))

デフォルト:0.35 秒

スプリングアニメーション

スプリングアニメーションは物理シミュレーションを使用して、自然で有機的な動きを作り出します。

組み込みのプリセット
.animation(.smooth)     // No bounce (default since iOS 17)
.animation(.snappy)     // Small amount of bounce
.animation(.bouncy)     // Larger amount of bounce
カスタムスプリング
.animation(.spring(duration: 0.6, bounce: 0.3))

パラメーター

  • duration — 体感上のアニメーション期間
  • bounce — バウンスの強さ(0 = バウンスなし、1 = 非常に弾む)

従来のスプリングパラメーター(質量、剛性、減衰)よりもはるかに直感的です。

高次アニメーション

基本のアニメーションを変更して、複雑なエフェクトを作成します。

遅延
.animation(.spring.delay(0.5))

アニメーションの開始前に 0.5 秒待機します。

繰り返し
.animation(.easeInOut.repeatCount(3, autoreverses: true))
.animation(.linear.repeatForever(autoreverses: false))

アニメーションを複数回、または無限に繰り返します。

速度
.animation(.spring.speed(2.0))  // 2x faster
.animation(.spring.speed(0.5))  // 2x slower

アニメーションの速度を倍増させます。

デフォルトのアニメーションの変更(iOS 17 以降)

iOS 17 より前

withAnimation {
    // Used timing curve by default
}

iOS 17 以降

withAnimation {
    // Uses .smooth spring by default
}

変更の理由:スプリングアニメーションはより自然に感じられ、中断されたときも速度が維持されます。

推奨事項:スプリングを積極的に活用しましょう。UI の応答性が高まり、洗練された印象になります。


パート 5:トランザクションシステム

withAnimation

アニメーションを開始する最も一般的な方法です。

Button("Scale Up") {
    withAnimation(.spring) {
        scale = 1.5
    }
}

仕組み

  1. withAnimation がトランザクションを開始します
  2. トランザクションの辞書にアニメーションを設定します
  3. クロージャーを実行します(状態の変更)
  4. トランザクションがビュー階層を下へ伝播します
  5. アニメーション可能な属性がアニメーションを確認し、値を補間します
明示的なアニメーション
withAnimation(.spring(duration: 0.6, bounce: 0.4)) {
    isExpanded.toggle()
}
アニメーションなし
withAnimation(nil) {
    // Changes happen immediately, no animation
    resetState()
}
animation() ビューモディファイア

ビュー内の特定の値にアニメーションを適用します。

基本的な使い方
Circle()
    .fill(isActive ? .blue : .gray)
    .animation(.spring, value: isActive)

仕組み:アニメーションは isActive が変化したときにのみ適用されます。ほかの状態変化では、このアニメーションは開始されません。

同じビューで複数のアニメーションを使用する
Circle()
    .scaleEffect(scale)
    .animation(.bouncy, value: scale)
    .opacity(opacity)
    .animation(.easeInOut, value: opacity)

異なるプロパティに異なるアニメーションを適用します。

スコープ付きアニメーション(iOS 17+)

アニメーション可能な特定の属性にアニメーションの適用範囲を絞ります。

従来の方法の問題点
struct AvatarView: View {
    var selected: Bool

    var body: some View {
        Image("avatar")
            .scaleEffect(selected ? 1.5 : 1.0)
            .animation(.spring, value: selected)
            // ⚠️ If image also changes when selected changes,
            //    image transition gets animated too (accidental)
    }
}
解決策:スコープ付きアニメーション
struct AvatarView: View {
    var selected: Bool

    var body: some View {
        Image("avatar")
            .animation(.spring, value: selected) {
                $0.scaleEffect(selected ? 1.5 : 1.0)
            }
            // ✅ Only scaleEffect animates, image transition doesn't
    }
}

仕組み

  • アニメーションはクロージャ内の属性にのみ適用されます
  • ほかの属性には影響しません
  • 意図しないアニメーションを防ぎます
カスタムトランザクションキー

カスタムの TransactionKey 型を定義し、トランザクションシステムを通じてコンテキストを伝播させます。withTransaction を使用して値を設定し、.transaction モディファイアを使用して値を読み取ります。これにより、状態変更のきっかけ(タップかプログラムによる変更か)に応じて異なるアニメーションを適用できます。


パート 6:高度なトピック

CustomAnimation プロトコル

独自のアニメーションアルゴリズムを実装します。

protocol CustomAnimation {
    // Calculate current value
    func animate<V: VectorArithmetic>(
        value: V,
        time: TimeInterval,
        context: inout AnimationContext<V>
    ) -> V?

    // Optional: Should this animation merge with previous?
    func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool

    // Optional: Current velocity
    func velocity<V: VectorArithmetic>(
        value: V,
        time: TimeInterval,
        context: AnimationContext<V>
    ) -> V?
}
例:線形タイミングカーブ
struct LinearAnimation: CustomAnimation {
    let duration: TimeInterval

    func animate<V: VectorArithmetic>(
        value: V,              // Delta vector: target - current
        time: TimeInterval,
        context: inout AnimationContext<V>
    ) -> V? {
        if time >= duration { return nil }
        return value.scaled(by: time / duration)
    }
}

重要なポイント:value はターゲットではなく、差分ベクトル(ターゲット − 現在値)です。アニメーションが完了したら nil を返します。SwiftUI がスケーリングされた差分を現在値に自動で加算します。

アニメーションのマージ動作

前のアニメーションが完了する前に新しいアニメーションが始まると、どうなるでしょうか?

タイミングカーブアニメーション(デフォルト:マージされない)
func shouldMerge(...) -> Bool {
    return false // Default implementation
}

動作:両方のアニメーションが同時に実行され、結果が加算的に組み合わされます。

例

  • 1回目のタップ:1.0 → 1.5 にアニメーション(実行中)
  • 2回目のタップ(完了前):1.5 → 1.0 にアニメーション
  • 結果:両方のアニメーションが実行され、値が組み合わされます
スプリングアニメーション(マージして再ターゲット)
func shouldMerge(...) -> Bool {
    return true // Springs override this
}

動作:新しいアニメーションは前のアニメーションの状態を引き継ぎ、速度を維持します。

例

  • 1回目のタップ:速度 V で 1.0 → 1.5 にアニメーション
  • 2回目のタップ(完了前):現在の速度 V を維持したまま、ターゲットを 1.0 に変更
  • 結果:速度が突然変化することなく、スムーズに遷移します

スプリングアニメーションがより自然に感じられる理由:中断されても勢いが維持されます。


パート 7:複数ステップのアニメーション(iOS 17+)

PhaseAnimator

一連のフェーズを順番に切り替え、各フェーズで異なるモディファイアを適用します。フェーズ間の各遷移は個別にアニメーション化されます。

PhaseAnimator([false, true]) { phase in
    Image(systemName: "star.fill")
        .scaleEffect(phase ? 1.5 : 1.0)
        .opacity(phase ? 1.0 : 0.5)
        .rotationEffect(.degrees(phase ? 360 : 0))
} animation: { phase in
    phase ? .spring(duration: 0.8, bounce: 0.3) : .easeInOut(duration: 0.4)
}

仕組み:最初のフェーズから始まり、2番目のフェーズへアニメーションし、その後ループします。animation クロージャは、そのフェーズへの遷移に使用するアニメーションを返します。フェーズには任意の Equatable 型を使用できます。複雑な複数ステップのシーケンスには enum を使用します。

enum PulsePhase: CaseIterable { case idle, expand, contract }

PhaseAnimator(PulsePhase.allCases) { phase in
    Circle()
        .scaleEffect(phase == .expand ? 1.3 : phase == .contract ? 0.9 : 1.0)
}

トリガー:trigger パラメーターを追加すると、連続してループするのではなく、値が変更されたときにのみアニメーションを実行できます。

KeyframeAnimator

正確なタイムラインベースのアニメーションを実現する、プロパティごとのキーフレームトラックを提供します。PhaseAnimator よりも細かく制御できます。

struct AnimationValues {
    var scale: Double = 1.0
    var rotation: Angle = .zero
    var yOffset: Double = 0
}

KeyframeAnimator(initialValue: AnimationValues()) { values in
    Image(systemName: "heart.fill")
        .scaleEffect(values.scale)
        .rotationEffect(values.rotation)
        .offset(y: values.yOffset)
} keyframes: { _ in
    KeyframeTrack(\.scale) {
        SpringKeyframe(1.5, duration: 0.3)
        SpringKeyframe(1.0, duration: 0.3)
    }
    KeyframeTrack(\.rotation) {
        LinearKeyframe(.degrees(15), duration: 0.15)
        LinearKeyframe(.degrees(-15), duration: 0.3)
        LinearKeyframe(.zero, duration: 0.15)
    }
    KeyframeTrack(\.yOffset) {
        CubicKeyframe(-20, duration: 0.3)
        CubicKeyframe(0, duration: 0.3)
    }
}

キーフレームの型:LinearKeyframe(一定の速度)、SpringKeyframe(スプリング物理)、CubicKeyframe(ベジェ曲線)、MoveKeyframe(補間なしの瞬間的なジャンプ)。

PhaseAnimator との比較:状態をシンプルに切り替える場合は PhaseAnimator を使用します。異なるプロパティに個別のタイミングが必要な場合は KeyframeAnimator を使用します。

.transition()

ビュー階層へのビューの挿入時や、階層からの削除時に、そのビューをどのようにアニメーション化するかを定義します。

if showDetail {
    DetailView()
        .transition(.slide)                          // Slide in/out
        .transition(.scale.combined(with: .opacity)) // Combine transitions
        .transition(.move(edge: .bottom))            // Move from edge
        .transition(.asymmetric(                     // Different in/out
            insertion: .scale.combined(with: .opacity),
            removal: .opacity
        ))
}

アニメーションコンテキストが必要です — 状態変更を withAnimation で囲むか、.animation() モディファイアを使用します。アニメーションがなければ、ビューは即座に表示または非表示になります。

matchedGeometryEffect

階層内の2つの位置間で、ビューのフレームをスムーズにアニメーションさせます。ヒーロー遷移や共有要素アニメーションでよく使用されます。

@Namespace private var animation

// Source
if !isExpanded {
    RoundedRectangle(cornerRadius: 10)
        .matchedGeometryEffect(id: "card", in: animation)
        .frame(width: 100, height: 100)
}

// Destination
if isExpanded {
    RoundedRectangle(cornerRadius: 20)
        .matchedGeometryEffect(id: "card", in: animation)
        .frame(width: 300, height: 400)
}

主なルール:同じ id と同じ Namespace の組み合わせで、対応するビューのペアになります。一度に、特定の ID を持つビューのうち isSource: true(デフォルト)にできるのは1つだけです。スムーズに補間するには、状態変更を withAnimation で囲みます。

contentTransition

ビュー内のテキストやシンボルのコンテンツ変更をアニメーション化します(iOS 16+)。

Text(value, format: .number)
    .contentTransition(.numericText(countsDown: value < previous))

Image(systemName: isFavorite ? "heart.fill" : "heart")
    .contentTransition(.symbolEffect(.replace))

パート 8:ズームトランジション(iOS 18+)

概要

iOS 18では、タップしたセルが遷移先のビューへ変形するズームトランジションが導入されました。このトランジションは常にインタラクティブで、トランジションの開始中または開始後にユーザーがビューをつかんでドラッグできます。

主なメリット 大きなセルから遷移するアプリの部分では、トランジション中も同じUI要素を画面に表示し続けることで、ズームトランジションが視覚的な連続性を高めます。

SwiftUIでの実装

ズームトランジションを使用するには、次の2つの手順を行います。

手順1:遷移先でトランジションスタイルを宣言する
NavigationLink {
    BraceletEditor(bracelet)
        .navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
} label: {
    BraceletPreview(bracelet)
}
手順2:遷移元ビューを指定する
BraceletPreview(bracelet)
    .matchedTransitionSource(id: bracelet.id, in: namespace)
完全な例
struct BraceletListView: View {
    @Namespace private var braceletList
    let bracelets: [Bracelet]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
                    ForEach(bracelets) { bracelet in
                        NavigationLink {
                            BraceletEditor(bracelet: bracelet)
                                .navigationTransition(
                                    .zoom(sourceID: bracelet.id, in: braceletList)
                                )
                        } label: {
                            BraceletPreview(bracelet: bracelet)
                        }
                        .matchedTransitionSource(id: bracelet.id, in: braceletList)
                    }
                }
            }
        }
    }
}
UIKitでの実装

プッシュ先のビューコントローラーでpreferredTransition = .zoom { context in ... }を設定します。クロージャーは遷移元ビューを返し、ズームインとズームアウトの両方で呼び出されます。ビューを直接ではなく、安定した識別子(モデルオブジェクト)をキャプチャしてください。

プレゼンテーション

ズームトランジションはfullScreenCoverとsheetでも使用できます。

.fullScreenCover(item: $selectedBracelet) { bracelet in
    BraceletEditor(bracelet: bracelet)
        .navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
}
遷移元ビューのスタイル設定
.matchedTransitionSource(id: bracelet.id, in: namespace) { source in
    source.cornerRadius(8.0).shadow(radius: 4)
}
流動的なトランジションのライフサイクル

プッシュトランジションはキャンセルできません。中断されると、ポップトランジションに切り替わります。ビューコントローラーは必ずAppeared状態になります。トランジションの重複を防ぐためのチェックは行わず、システムに任せてください。


パート9:UIKit/AppKitアニメーションのブリッジ(iOS 18以降)

概要

iOS 18では、SwiftUIのAnimation型を使ってUIKitおよびAppKitのビューをアニメーション化できます。カスタムアニメーションを含む、SwiftUIの幅広いアニメーションを利用できます。

基本的な使い方
// Old way
UIView.animate(withDuration: 0.5, delay: 0,
               usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
    bead.center = endOfBracelet
}

// New way: Use SwiftUI Animation type
UIView.animate(.spring(duration: 0.5)) {
    bead.center = endOfBracelet
}

.linear、.easeIn/Out、.spring、.smooth、.snappy、.bouncy、.repeatForever()、カスタムアニメーションなど、すべてのSwiftUIアニメーションが使用できます。

アーキテクチャに関する注意:従来のUIKit APIとは異なり、CAAnimationは生成されず、プレゼンテーション値が直接アニメーション化されます。


パート10:UIViewRepresentableアニメーションのブリッジ(iOS 18以降)

問題

UIKitビューをSwiftUIでラップしても、アニメーションは自動的にはブリッジされません。

struct BeadBoxWrapper: UIViewRepresentable {
    @Binding var isOpen: Bool

    func updateUIView(_ box: BeadBox, context: Context) {
        // ❌ Animation on binding doesn't affect UIKit
        box.lid.center.y = isOpen ? -100 : 100
    }
}

// Usage
BeadBoxWrapper(isOpen: $isOpen)
    .animation(.spring, value: isOpen)  // No effect on UIKit view
解決策:context.animate()

context.animate()を使ってSwiftUIアニメーションをブリッジします。

struct BeadBoxWrapper: UIViewRepresentable {
    @Binding var isOpen: Bool

    func makeUIView(context: Context) -> BeadBox {
        BeadBox()
    }

    func updateUIView(_ box: BeadBox, context: Context) {
        // ✅ Bridges animation from Transaction to UIKit
        context.animate {
            box.lid.center.y = isOpen ? -100 : 100
        }
    }
}
仕組み
  1. SwiftUIは現在のTransactionにアニメーション情報を保存します
  2. context.animate()がTransactionのアニメーションを読み取ります
  3. そのアニメーションをクロージャー内のUIViewの変更に適用します
  4. Transactionにアニメーションがない場合、変更は即座に行われます(アニメーションなし)
主な動作
context.animate {
    // Changes here
} completion: {
    // Called when animation completes
    // If not animated, called immediately inline
}

アニメーションの有無にかかわらず動作します — 常にこのパターンを使っても問題ありません。

完全な同期

SwiftUIビューとUIViewをまたいで実行される1つのアニメーションは、完全に同期します。これにより、異なる階層をシームレスに組み合わせられます。


パート11:ジェスチャー駆動のアニメーション(iOS 18以降)

速度の自動保持

SwiftUIアニメーションは、アニメーションのマージを通じて速度を自動的に保持するため、手動で速度を計算する必要はありません。

// UIKit with SwiftUI animations
func handlePan(_ gesture: UIPanGestureRecognizer) {
    switch gesture.state {
    case .changed:
        UIView.animate(.interactiveSpring) {
            bead.center = gesture.location(in: view)
        }
    case .ended:
        UIView.animate(.spring) {  // Inherits velocity automatically
            bead.center = endOfBracelet
        }
    default: break
    }
}

// Pure SwiftUI equivalent
DragGesture()
    .onChanged { value in
        withAnimation(.interactiveSpring) { position = value.location }
    }
    .onEnded { _ in
        withAnimation(.spring) { position = targetPosition }
    }

各.interactiveSpringは前のアニメーションのターゲットを更新し、最後の.springは累積した速度を引き継いで滑らかに減速します。



トラブルシューティング

プロパティがアニメーション化されない

次の順に確認してください。

  1. 型はVectorArithmeticに準拠していますか? — Intはアニメーション化できません。DoubleまたはFloatを使用してください
  2. アニメーション修飾子がありますか? — .animation(.spring, value: x)またはwithAnimationが必要です
  3. 正しい値を追跡していますか? — .animation(.spring, value: progress)を使い、.animation(.spring, value: title)は使わないでください
  4. ビューはAnimatableに準拠していますか? — カスタムビューには@Animatable(iOS 26以降)または手動でのanimatableDataが必要です
アニメーションがカクつく

カスタムのAnimatable準拠では、メインスレッド上で毎フレームbodyが呼び出されます。可能な場合は組み込みエフェクト(.opacity()、.scaleEffect())を使用してください。これらはメインスレッド以外で実行されます。複雑なケースでは、Instrumentsでプロファイルを確認してください。

アニメーションが予期せずマージされる

スプリングアニメーションはデフォルトでマージされ、速度が保持されます。マージ動作を避けたい場合は、タイミングカーブアニメーション(.easeInOut)を使用してください。上記のアニメーションのマージ動作セクションを参照してください。

リソース

WWDC:2023-10156, 2023-10157, 2023-10158, 2024-10145, 2025-256

ドキュメント:/swiftui/animatable, /swiftui/animation, /swiftui/vectorarithmetic, /swiftui/transaction, /swiftui/view/navigationtransition(:), /swiftui/view/matchedtransitionsource(id:in:configuration:), /uikit/uiview/animate(:changes:completion:)

スキル:axiom-swiftui-26-ref, axiom-swiftui-nav-ref, axiom-swiftui-performance, axiom-swiftui-debugging, axiom-sf-symbols-ref

コメント