mcpField
S
swiftui-animation

swiftui-animation

github.com/dpearson2699デザイン・UI1.2k5.5k 回インストール13 週間前に更新
リポジトリ

swiftui-animation について

SwiftUI 開発者向けに、アニメーションのレビュー、作成、修正を支援するスキルです。最新の API、タイミング、トランジション、アクセシビリティへの配慮を扱い、「視差効果を減らす」設定への対応も確認します。

主な機能

  1. 01アニメーション API の選択
  2. 02状態駆動・フェーズアニメーション
  3. 03キーフレームと共有要素の遷移
  4. 04SF Symbols の効果と描画
  5. 05「視差効果を減らす」の確認

活用シーン

  1. 01状態変化にアニメーションを追加
  2. 02ビューの挿入・削除を演出
  3. 03アニメーション範囲や性能の問題を調査
  4. 04「視差効果を減らす」への対応を確認

インストール

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

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-animation

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
animation-advanced.md
core-animation-bridge.md

詳細

SwiftUI アニメーション(iOS 26以降)

SwiftUI アニメーションをレビュー、作成、修正します。Swift 6.3 のパターンを使用し、適切なタイミング、トランジション、アクセシビリティへの対応を備えた最新のアニメーション API を適用します。

目次

トリアージワークフロー

ステップ 1:アニメーションの種類を特定する
種類API使用場面
状態駆動型withAnimation, .animation(_:body:), .animation(_:value:)明示的な状態変更、特定のモディファイアのアニメーション化、または単純な値にバインドされた変更
複数フェーズPhaseAnimator順序立てた複数ステップのアニメーション
キーフレームKeyframeAnimator複数のプロパティを使った複雑な振り付け
共有要素matchedGeometryEffectレイアウトに基づくヒーロートランジション
ナビゲーションmatchedTransitionSource + .navigationTransition(.zoom)NavigationStack のプッシュ/ポップ時のズーム
ビューのライフサイクル.transition()挿入と削除
テキストコンテンツ.contentTransition()テキストや数値のその場での変更
シンボル.symbolEffect()SF Symbol のアニメーション
カスタムCustomAnimation protocol独自のタイミングカーブ
Core Animation ブリッジCALayer, CAAnimation, CADisplayLinkアドバイスを行う前に references/core-animation-bridge.md を参照してください
ステップ 2:アニメーションカーブを選択する
.easeInOut(duration: 0.3)       // mechanical timing
.smooth                         // fluid, no bounce
.snappy                         // responsive, small bounce
.bouncy                         // playful, visible bounce
.spring(duration: 0.5, bounce: 0.3)

プリセットでは意図した動きを表現できない場合は、高度なカタログを使用してください。

ステップ 3:適用して確認する
  • アニメーションが正しい状態変更をきっかけに開始されることを確認します。
  • アクセシビリティの「視差効果を減らす」を有効にしてテストします。
  • アニメーションのコンテンツクロージャ内で、コストの高い処理が実行されていないことを確認します。
  • CA ブリッジでは、デリゲートに Coordinator を使用し、ディスプレイリンクを無効化し、フレームレート範囲は目安として扱い、実際のリフレッシュレートに応じて処理を調整します。

withAnimation(明示的なアニメーション)

withAnimation(.spring) { isExpanded.toggle() }

// With completion (iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
    isExpanded = true
} completion: { loadContent() }

暗黙的なアニメーション

状態変更の所有権には withAnimation、選択したモディファイアには .animation(_:body:)、単純な値にバインドされた変更には .animation(_:value:) を使用します。

Badge()
    .foregroundStyle(isActive ? .green : .secondary)
    .animation(.snappy) { content in
        content
            .scaleEffect(isActive ? 1.15 : 1.0)
            .opacity(isActive ? 1.0 : 0.7)
    }
Circle()
    .scaleEffect(isActive ? 1.2 : 1.0)
    .opacity(isActive ? 1.0 : 0.6)
    .animation(.bouncy, value: isActive)

Spring 型(iOS 17以降)

知覚ベースの形式またはプリセットを優先してください。物理的なパラメーター、応答ベースのパラメーター、または整定パラメーターが必要な場合にのみ、高度なリファレンスを読み込んでください。

Spring(duration: 0.5, bounce: 0.3)
Spring.smooth
Spring.snappy
Spring.bouncy

PhaseAnimator(iOS 17以降)

フェーズごとのアニメーションカーブを使い、離散的なフェーズを順に切り替えます。

enum PulsePhase: CaseIterable {
    case idle, grow, shrink
}

struct PulsingDot: View {
    var body: some View {
        PhaseAnimator(PulsePhase.allCases) { phase in
            Circle()
                .frame(width: 40, height: 40)
                .scaleEffect(phase == .grow ? 1.4 : 1.0)
                .opacity(phase == .shrink ? 0.5 : 1.0)
        } animation: { phase in
            switch phase {
            case .idle: .easeIn(duration: 0.2)
            case .grow: .spring(duration: 0.4, bounce: 0.3)
            case .shrink: .easeOut(duration: 0.3)
            }
        }
    }
}

トリガーベースのバリエーションでは、トリガーが変更されるたびに次のフェーズへ進みます。

PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
    // ...
} animation: { _ in .spring(duration: 0.4) }

KeyframeAnimator(iOS 17以降)

複数のプロパティを、それぞれ独立したタイムラインに沿ってアニメーション化します。

struct AnimValues {
    var scale: Double = 1.0
    var yOffset: Double = 0.0
    var opacity: Double = 1.0
}

struct BounceView: View {
    @State private var trigger = false

    var body: some View {
        Button { trigger.toggle() } label: {
            Image(systemName: "star.fill")
                .font(.largeTitle)
                .keyframeAnimator(
                    initialValue: AnimValues(),
                    trigger: trigger
                ) { content, value in
                    content
                        .scaleEffect(value.scale)
                        .offset(y: value.yOffset)
                        .opacity(value.opacity)
                } keyframes: { _ in
                    KeyframeTrack(\.scale) {
                        SpringKeyframe(1.5, duration: 0.3)
                        CubicKeyframe(1.0, duration: 0.4)
                    }
                    KeyframeTrack(\.yOffset) {
                        CubicKeyframe(-30, duration: 0.2)
                        CubicKeyframe(0, duration: 0.4)
                    }
                    KeyframeTrack(\.opacity) {
                        LinearKeyframe(0.6, duration: 0.15)
                        LinearKeyframe(1.0, duration: 0.25)
                    }
                }
        }
        .buttonStyle(.plain)
    }
}

キーフレームの種類:LinearKeyframe(線形)、CubicKeyframe(滑らかなカーブ)、SpringKeyframe(スプリング物理)、MoveKeyframe(瞬時のジャンプ)。

ループするキーフレームアニメーションには repeating: true を使用します。 Swift 6 では、キーフレームのクロージャは @Sendable です。モディファイアの前に、状態値や環境値をキャプチャしてください。

@Animatable Macro

手動で記述する AnimatableData の定型コードを置き換えます。アニメーション可能な保存プロパティを持つ任意の型に付与します。

@Animatable
struct WaveShape: Shape {
    var frequency: Double
    var amplitude: Double
    var phase: Double
    @AnimatableIgnored var lineWidth: CGFloat

    func path(in rect: CGRect) -> Path {
        // draw wave using frequency, amplitude, phase
    }
}

ルール:

  • 保存プロパティは VectorArithmetic に準拠する必要があります。
  • アニメーション不可能なプロパティを除外するには @AnimatableIgnored を使用します。
  • 計算プロパティは含まれません。

matchedGeometryEffect(iOS 14+)

共有要素アニメーションのために、ビュー間でジオメトリを同期します。

struct HeroView: View {
    @Namespace private var heroSpace
    @State private var isExpanded = false

    var body: some View {
        Group {
            if isExpanded {
                Button {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = false
                    }
                } label: {
                    DetailCard()
                        .matchedGeometryEffect(id: "card", in: heroSpace)
                }
            } else {
                Button {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = true
                    }
                } label: {
                    ThumbnailCard()
                        .matchedGeometryEffect(id: "card", in: heroSpace)
                }
            }
        }
        .buttonStyle(.plain)
    }
}

ID ごとに表示されるソースビューは 1 つだけにしてください。それ以外の場合、結果は未定義です。

Navigation Zoom Transition(iOS 18+)

ソースビューの matchedTransitionSource と、遷移先の .navigationTransition(.zoom(...)) を組み合わせます。

struct GalleryView: View {
    @Namespace private var zoomSpace
    let items: [GalleryItem]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(items) { item in
                        NavigationLink {
                            GalleryDetail(item: item)
                                .navigationTransition(
                                    .zoom(sourceID: item.id, in: zoomSpace)
                                )
                        } label: {
                            ItemThumbnail(item: item)
                                .matchedTransitionSource(
                                    id: item.id, in: zoomSpace
                                )
                        }
                    }
                }
            }
        }
    }
}

.navigationTransition は内側のコンテナーではなく、遷移先のビューに適用してください。

Transitions(iOS 17+)

ビューの挿入時と削除時のアニメーションを制御します。

if showBanner {
    BannerView()
        .transition(.move(edge: .top).combined(with: .opacity))
}

組み込みの一覧とカスタム Transition の例については、 すべての Transition の種類 を参照してください。

非対称なトランジション:

.transition(.asymmetric(
    insertion: .push(from: .bottom),
    removal: .opacity
))

ContentTransition(iOS 16+)

挿入や削除を伴わず、コンテンツの変更をその場でアニメーションします。

Text("\(score)")
    .contentTransition(.numericText(countsDown: false))
    .animation(.snappy, value: score)

// For SF Symbols
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
    .contentTransition(.symbolEffect(.replace.downUp))

種類:.identity、.interpolate、.opacity、 .numericText(countsDown:)、.numericText(value:)、.symbolEffect。

Symbol Effects(iOS 17+)

セマンティックエフェクトを使って SF Symbols をアニメーションします。.bounce、.pulse、.variableColor、 .scale、.appear、.disappear、.replace は iOS 17+ で利用できます。.breathe、 .rotate、.wiggle には iOS 18+ が必要です。

// Discrete (triggers on value change)
Image(systemName: "bell.fill").symbolEffect(.bounce, value: notificationCount)

// iOS 18+
Image(systemName: "arrow.clockwise")
    .symbolEffect(.wiggle.clockwise, value: refreshCount)

// Indefinite (active while condition holds)
Image(systemName: "wifi").symbolEffect(.pulse, isActive: isSearching)

// iOS 18+
Image(systemName: "mic.fill")
    .symbolEffect(.breathe, isActive: isRecording)

// Variable color with chaining
Image(systemName: "speaker.wave.3.fill")
    .symbolEffect(
        .variableColor.iterative.reversing.dimInactiveLayers,
        options: .repeating,
        isActive: isPlaying
    )

スコープ:.byLayer、.wholeSymbol。方向はエフェクトごとに異なります。

Symbol Rendering Modes

.symbolRenderingMode(_:) を使って .monochrome、.hierarchical、.multicolor、.palette を選択し、 パレットの色を指定するには .foregroundStyle を使用します。

Variable symbols: 割合に応じた塗りつぶしには Image(systemName:variableValue:)(iOS 16+)を使用します。.symbolVariableValueMode(_:)(iOS 26+)を使うと、.draw または .color を選択できます。

Image(systemName: "wifi", variableValue: signalStrength) // 0.0...1.0
    .symbolVariableValueMode(.draw) // iOS 26+

ドキュメント: SymbolRenderingMode · symbolRenderingMode(_:) · Image(systemName:variableValue:) · symbolVariableValueMode(_:)

よくある間違い

1. 正確なスコープが必要な場面で、単独の .animation(_:) を使う
// TOO BROAD — applies when the view changes
.animation(.easeIn)

.animation(.easeIn, value: isVisible) // CORRECT: value-bound

// CORRECT — scope animation to selected modifiers
.animation(.easeIn) { content in
    content.opacity(isVisible ? 1.0 : 0.0)
}

withAnimation(.easeIn) { isVisible.toggle() } // CORRECT: own mutation
2. アニメーションのクロージャー内でコストの高い処理やアクター分離された値の読み取りを行う

keyframeAnimator / PhaseAnimator のコンテンツクロージャーは、毎フレーム実行されます。コストの高い値は事前に計算し、視覚的なプロパティのみをアニメーションし、@Sendable なキーフレームクロージャーの前に状態や環境の値をキャプチャしてください。

3. Reduce Motion のサポートがない

シンボルでは継承されたエフェクトを削除し、大きな動きは reduceMotion ? .none : animation で制御します。

@Environment(\.accessibilityReduceMotion) private var reduceMotion
Image(systemName: "wifi").symbolEffect(.pulse, isActive: isSearching).symbolEffectsRemoved(reduceMotion)
4. matchedGeometryEffect のソースが複数ある

ID ごとに、一度に表示するソースビューは 1 つだけにしてください。同じ ID を持つ複数のソースが表示されると、レイアウトは未定義になります。

5. DispatchQueue または UIView.animate を使う
// WRONG
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isVisible = true } }
// CORRECT
withAnimation(.spring.delay(0.5)) { isVisible = true }
6. ContentTransition にアニメーションを指定し忘れる
// WRONG — no animation, content transition has no effect
Text("\(count)").contentTransition(.numericText(countsDown: true))
// CORRECT — pair with animation
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))
    .animation(.snappy, value: count)
7. navigationTransition を誤ったビューに適用する

.navigationTransition(.zoom(sourceID:in:)) はコンテナーの内側ではなく、最外側の遷移先ビューに適用してください。

レビューチェックリスト

  • アニメーションカーブが意図に合っている(自然な動きには spring、機械的な動きには ease)
  • withAnimation で状態変更を囲んでいる。暗黙的なアニメーションには、修飾子のスコープを選択的に指定する .animation(_:body:)、または明示的な値を指定する .animation(_:value:) を使用している
  • matchedGeometryEffect のソースは ID ごとに 1 つだけであり、ズームでは id / namespace が一致している
  • 合成で対応できる場合は @Animatable マクロを使用し、手動の animatableData はカスタムパッキングの方が明確な場合にのみ使用している
  • accessibilityReduceMotion を確認している。DispatchQueue / UIView.animate は使用していない
  • トランジションには .transition() を使用している。contentTransition はアニメーションと組み合わせ、要件を満たす最も狭い暗黙的アニメーションのスコープを使用している
  • アニメーションされる状態は @MainActor 上で変更している。アニメーションを駆動する型は Sendable である

参考資料

  • CustomAnimation プロトコル、Spring のバリエーション、Transition の種類、シンボルエフェクト、Transaction システム、UnitCurve、パフォーマンスに関するガイダンスについては、references/animation-advanced.md を参照してください。Core Animation とのブリッジパターンについては、references/core-animation-bridge.md を参照してください。

コメント