mcpField
S
swiftui-motion

swiftui-motion

github.com/athevonデザイン・UI698940 回インストール3 週間前に更新
リポジトリ

swiftui-motion について

Claude Code、claude.ai、Cowork向けのSwiftUIアニメーションスキルです。API選択、スプリング、トランジション、ジェスチャーの実装指針に加え、アクセシビリティや性能上の注意点も扱います。

主な機能

  1. 01SwiftUIアニメーションAPIの選択
  2. 02スプリングとトランジションの指針
  3. 03ジェスチャー連動アニメーション
  4. 04視差効果を減らすアクセシビリティ規則
  5. 05よくあるアニメーションのアンチパターン

活用シーン

  1. 01ビューに適したアニメーションAPIの選定
  2. 02連動する状態遷移の構築
  3. 03ジェスチャー操作の追加
  4. 04視差効果を減らす設定の確認

インストール

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

$npx skills add https://github.com/athevon/genjutsu --skill swiftui-motion

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

詳細

バージョン依存の情報です。 以下の API 名、SDK の要件、ブラウザーサポートに関する記述はすべて、一次情報に基づき 2026-09-08 に確認済みです。何を、いつ確認したかは _jutsu/VERSIONS.md に記載されています。この日付が古い場合は、バージョン番号に基づいて作業する前に再確認してください。

SwiftUI モーション

SwiftUI アニメーションのコアです。SwiftUI プロジェクト(iOS、macOS、Apple のマルチターゲット)で読み込まれます。 ここでは簡潔なルールを紹介します。詳細は references/ を参照してください。 ../motion-principles/SKILL.md(基礎)および ../mobile-principles/SKILL.md(タッチ UX)と併用してください。


アニメーション API の選択ツリー

要件API
時間経過に伴う単一の値withAnimation { } + @State または .animation(_, value:)
複数の状態を連携させるPhaseAnimator(phases)(iOS 17 以降)
時間ベースのキーフレームKeyframeAnimator(initialValue:repeating:content:)(iOS 17 以降)
カスタムプロパティアニメーション@Animatable マクロ(コンパイルには Xcode 26 / SwiftUI 2025 SDK が必要です。ランタイムでの利用可能範囲は iOS 13.0 以降 / macOS 10.15 以降 / watchOS 6.0 以降)、または手動で Animatable プロトコルを実装(iOS 13 以降)
共有要素のトランジションmatchedGeometryEffect(id:in:)
ジェスチャー駆動DragGesture / MagnifyGesture + .offset / .scaleEffect
無限ループ.animation(.linear.repeatForever(autoreverses: true), value: ...) または .phaseAnimator

ルール: まずは withAnimation を使ってください。順序のある状態が 3 つ以上ある場合に限り、PhaseAnimator を使います。並列の時間ベースのトラックが必要な場合に限り、KeyframeAnimator を使います。


スプリング(気にするべきイージングはこれだけです)

SwiftUI には、名前付きのスプリングプリセット(iOS 17 以降)として .snappy、.bouncy、.smooth のほか、パラメーターを指定する .spring(duration:bounce:blendDuration:) と .interactiveSpring(...) が用意されています。名前付きの 3 種類はいずれも (duration:extraBounce:) のオーバーロードも利用できます。これらを使ってください。プリセットが適切でない場合に限り、response / dampingFraction を調整します。

プリセット(iOS 17 以降)同等の設定印象
.snappy.spring(response: 0.5, dampingFraction: 0.85)キビキビした UI
.bouncy.spring(response: 0.5, dampingFraction: 0.7)遊び心のある動き
.smooth.spring(response: 0.5, dampingFraction: 1.0)落ち着いた、跳ねない動き
.interactiveSpring().spring(response: 0.15, dampingFraction: 0.86)ジェスチャーに追従

response はスプリングの周期で、剛性を時間で表したものです。つまり、1 回の振動にかかる時間です。収束時間ではありません。たとえば .spring(response: 0.5, dampingFraction: 1.0) は、0.5 秒を大きく超えても動き続けます。値が小さいほどキビキビし、大きいほど柔らかくなります。dampingFraction は 0...1 の範囲でオーバーシュートの強さを表します(1 = オーバーシュートなし、0 = 振動し続けます。0 は決して使わないでください)。UI の作業では、response: 0.2...0.5、dampingFraction: 0.7...1.0 の範囲にしてください。詳しくは references/springs-cheatsheet.md を参照してください。

iOS 17 以降では、bounce が 0...1 の .spring(duration:bounce:) も利用できます(0 = 臨界減衰、1 = 最大限に跳ねる)。これは同じスプリングを、デザイナーにも分かりやすい形で公開したものです。

.animation(.spring(duration: 0.4, bounce: 0.3), value: state)

暗黙的アニメーションと明示的アニメーション

// Implicit - via .animation modifier (binds to a value)
Circle()
    .scaleEffect(scale)
    .animation(.spring(.snappy), value: scale)
// Explicit - via withAnimation block
Button("Grow") {
    withAnimation(.smooth) { scale = 1.5 }
}

ルール: ユーザー操作によって引き起こされる状態変更には、明示的なアニメーション(withAnimation)を優先してください。値の変化を常にアニメーションさせる場合(例:どこから更新されてもアニメーションするプログレスバー)には、暗黙的なアニメーションを使います。同じプロパティに両方を使わないでください。外側の withAnimation が優先されますが、暗黙的な .animation 修飾子も実行され、分かりにくい形で重なります。


トランジション

トランジションは、if、switch、ForEach 内のビューの挿入・削除を制御します。親のアニメーションコンテキストが発生したときに実行されるため、状態変更を withAnimation で囲んでください。

if visible {
    Card().transition(.asymmetric(
        insertion: .move(edge: .bottom).combined(with: .opacity),
        removal: .opacity.animation(.easeIn(duration: 0.15))
    ))
}

悪い例 — ブラックホールに消えてしまいます:

Card().transition(.scale)  // scales to 0, the universal "broken" feel

良い例 — スケールを 0 にしないでください:

Card().transition(
    .scale(scale: 0.95).combined(with: .opacity)
)

iOS 17 以降では、Transition プロトコルを使ってカスタムトランジションを作成できる .transition(_:) 修飾子もあります。多くのビューでタイミングを共有する場合に便利です。作業の 90% では、組み込みのコンビネーター(.move、.opacity、.scale、.slide、.push、.asymmetric、.combined(with:))で十分です。


matchedGeometryEffect(ヒーローアニメーション)

同じNamespace内の2つのビューに、同じidを指定します。元のビューが置き換わる際、SwiftUIはフレームと位置を補間します。

struct Gallery: View {
    @Namespace private var ns
    @State private var expanded = false

    var body: some View {
        ZStack {
            if expanded {
                LargeCard()
                    .matchedGeometryEffect(id: "card", in: ns)
                    .onTapGesture { withAnimation(.spring(.smooth)) { expanded = false } }
            } else {
                SmallCard()
                    .matchedGeometryEffect(id: "card", in: ns)
                    .onTapGesture { withAnimation(.spring(.smooth)) { expanded = true } }
            }
        }
    }
}

isSource: true(基準となるビューではデフォルト)を指定すると、SwiftUIはどのフレームから補間するかを判断します。よくある注意点は、無関係な名前空間間でのIDの衝突、ビューIDの不安定さ(配列のインデックスではなく安定したIDを使います)、および遷移先のビューがまだ存在しないifブランチからアニメーションで退出することです(両方のブランチを同じ親ビューで囲み、ビューを削除するのではなく、不透明度を使って非表示にします)。


PhaseAnimator(iOS 17以降)

順序のある状態遷移に使います。このAPIは、Equatable(Hashableではありません)に制約されたフェーズ型のsome Sequenceを受け取ります。CaseIterableは、Phase.allCasesを渡せるようにするための便宜的な機能にすぎません。SwiftUIはシーケンスを順にたどり、最後の要素で落ち着きます。

enum SuccessPhase: CaseIterable { case start, scaleUp, rotate, settle }

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

    var body: some View {
        Image(systemName: "checkmark.circle.fill")
            .font(.system(size: 64))
            .foregroundStyle(.green)
            .phaseAnimator(SuccessPhase.allCases, trigger: trigger) { view, phase in
                view
                    .scaleEffect(phase == .start ? 0 : phase == .settle ? 1 : 1.2)
                    .rotationEffect(.degrees(phase == .rotate ? 360 : 0))
                    .opacity(phase == .start ? 0 : 1)
            } animation: { phase in
                switch phase {
                case .start: .smooth(duration: 0.05)
                case .scaleUp: .spring(.bouncy, blendDuration: 0.25)
                case .rotate: .spring(response: 0.4, dampingFraction: 0.8)
                case .settle: .smooth(duration: 0.2)
                }
            }
            .onTapGesture { trigger.toggle() }
    }
}

trigger:は省略可能です。省略すると、表示時にフェーズが自動的に一度進みます。ボタンのタップやモデルの更新など、外部からのシグナルが必要な場合に使用します。フェーズは順番に実行され、並列には実行されません。並列処理が必要な場合は、KeyframeAnimatorを使います。


KeyframeAnimator(iOS 17以降)

並列トラックを使った、時間ベースの連続アニメーションに使います。各KeyframeTrackは1つのキーパスを個別にアニメーション化し、SwiftUIはそれらすべてを同時に実行します。

struct AnimationValues {
    var scale: Double = 1
    var rotation: Angle = .zero
    var opacity: Double = 1
}

struct HeartTap: View {
    @State private var counter = 0

    var body: some View {
        Image(systemName: "heart.fill")
            .font(.system(size: 64))
            .foregroundStyle(.pink)
            .keyframeAnimator(initialValue: AnimationValues(), trigger: counter) { content, values in
                content
                    .scaleEffect(values.scale)
                    .rotationEffect(values.rotation)
                    .opacity(values.opacity)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    SpringKeyframe(1.3, duration: 0.15)
                    SpringKeyframe(1.0, duration: 0.3, spring: .bouncy)
                }
                KeyframeTrack(\.rotation) {
                    CubicKeyframe(.degrees(15), duration: 0.1)
                    CubicKeyframe(.degrees(-15), duration: 0.2)
                    CubicKeyframe(.degrees(0), duration: 0.15)
                }
            }
            .onTapGesture { counter += 1 }
    }
}

キーフレーム型は4種類あります。LinearKeyframe(点間を一定速度で移動)、SpringKeyframe(スプリングで収束)、CubicKeyframe(三次ベジェイージング)、MoveKeyframe(補間せず、瞬時に切り替え)。値の変化をトリガーとしてアニメーションを再実行します。詳しくは、references/phase-keyframe-deep.mdを参照してください。


Animatable / @Animatable

補間が必要なカスタム描画に使います。@Animatableマクロ(WWDC25でiOS 26 SDKとともに導入されたため、Xcode 26以降が必要です。宣言されている対応バージョンはiOS 13.0以降/macOS 10.15以降/watchOS 6.0以降のため、旧バージョンにもデプロイできます)は、構造体/クラス/列挙型に付けると、アニメーション可能な格納プロパティ(Double、CGFloat、Angle、CGSize、CGPoint、UnitPoint、および任意のVectorArithmetic)から、Animatableへの適合とanimatableDataの両方を合成します。格納プロパティに@AnimatableIgnored(同じ対応バージョン)を付けると、合成されるデータから除外できます。古いツールチェーンでは、引き続きanimatableDataを手動で記述できます。

struct ProgressRing: Shape {
    var progress: Double  // 0...1

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

    func path(in rect: CGRect) -> Path {
        var p = Path()
        p.addArc(
            center: CGPoint(x: rect.midX, y: rect.midY),
            radius: rect.width / 2,
            startAngle: .degrees(-90),
            endAngle: .degrees(-90 + 360 * progress),
            clockwise: false
        )
        return p
    }
}

ProgressRing(progress: progress)
    .stroke(.tint, lineWidth: 6)
    .animation(.spring(.smooth), value: progress)

複数のプロパティを持つシェイプでは、AnimatablePair<A, B>(またはネストしたペア)をanimatableDataとして使います。@Animatableマクロを使えば、この定型コードは不要です。シェイプに注釈を付けると、VectorArithmeticに適合する各格納プロパティ(Double、CGFloat、Angle、CGSize、CGPoint、UnitPointなど)が、自動的に合成されるanimatableDataにまとめられます。プロパティを除外するには、@AnimatableIgnoredを指定します。


ジェスチャ

ジェスチャ型用途
TapGesture離散タップ、ダブルタップ(count: 2)
LongPressGesture離散+連続(onEnded/onChanged)コンテキストメニュー、長押し録音
DragGesture連続ドラッグ、スワイプして閉じる
MagnifyGesture連続ピンチしてズーム(iOS 17以降、MagnificationGestureの後継)
RotateGesture連続回転(iOS 17以降、RotationGestureの後継)
SpatialTapGesture(iOS 16以降、macOS 13以降)離散位置情報を含むタップ
let tap = TapGesture().onEnded { print("tap") }
let drag = DragGesture().onChanged { value in offset = value.translation }
ZStack { ... }
    .gesture(tap.simultaneously(with: drag))

合成演算子は3つあります。.simultaneously(with:)(並列に認識)、.sequenced(before:)(一方が完了してからもう一方を実行)、.exclusively(before:)(どちらか一方のみで、両方ではない)。子ビューがジェスチャを認識できるタイミングを細かく制御するには、.gesture、.subviews、.all、.noneを指定して.simultaneousGesture(_, including: GestureMask)を使います。詳しくは、references/gestures-swiftui.mdを参照してください。


アンチパターン(BAD/GOOD)

1. 非推奨の.animation形式(値のバインディングなし)
// BAD - implicit-anim-everywhere, deprecated in iOS 15+
Circle().scaleEffect(scale).animation(.easeInOut)
// GOOD - bind to a specific value
Circle().scaleEffect(scale).animation(.easeInOut, value: scale)
// OR
withAnimation(.easeInOut) { scale = 1.5 }
2. フレームサイズを直接アニメーション化する
// BAD - .frame() drives layout pass every frame, drops fps under load
Card().frame(height: expanded ? 400 : 100)
    .animation(.spring(), value: expanded)
// GOOD - animate transform-equivalents (scale, offset) that the compositor handles
Card()
    .frame(height: 400)
    .scaleEffect(expanded ? 1 : 0.4, anchor: .top)
    .animation(.spring(), value: expanded)
// OR for actual layout transitions, use matchedGeometryEffect
3. 0にスケールする(無に消える罠)
// BAD - element vanishes into a black hole, feels broken
Card().transition(.scale)
// GOOD - minimum scale 0.9-0.95 + opacity
Card().transition(.scale(scale: 0.95).combined(with: .opacity))
4. body内のwithAnimation
// BAD - body runs on every render, animation re-fires arbitrarily
var body: some View {
    let _ = withAnimation(.spring()) { scale = 1.2 }  // never do this
    Circle().scaleEffect(scale)
}
// GOOD - trigger from user actions or .onChange
var body: some View {
    Circle()
        .scaleEffect(scale)
        .onTapGesture {
            withAnimation(.spring()) { scale = scale == 1 ? 1.2 : 1 }
        }
}

動きの軽減への対応

必須です。SwiftUI では、iOS / macOS のアクセシビリティ設定「視差効果を減らす」を環境経由で利用できます。プラットフォーム横断の理由については、../motion-principles/SKILL.md を参照してください。

struct Hero: View {
    @Environment(\.accessibilityReduceMotion) var reduceMotion
    @State private var shown = false

    var body: some View {
        Text("Welcome")
            .opacity(shown ? 1 : 0)
            .offset(y: shown ? 0 : (reduceMotion ? 0 : 20))
            .animation(reduceMotion ? .none : .spring(.smooth), value: shown)
            .onAppear { shown = true }
    }
}

ルール:動きの軽減が有効な場合でも、クロスフェードと不透明度の変化は許可されます。大きな移動、ゼロからの拡大、視差効果、ループする動きは無効にする必要があります。


クイックリファレンス:サブスキルの読み込み

必要な内容読み込むファイル
スプリングの詳細(response/dampingFraction の調整、雰囲気)references/springs-cheatsheet.md
PhaseAnimator と KeyframeAnimator を使った複雑なシーケンスreferences/phase-keyframe-deep.md
ジェスチャーの合成と競合の解決references/gestures-swiftui.md
高度なビジュアル(Metal、Liquid Glass)../swiftui-graphics/SKILL.md
プラットフォーム横断の UX(モバイル、デスクトップ)../mobile-principles/SKILL.md、../desktop-principles/SKILL.md
基礎(タイミング、イージング、アクセシビリティ)../motion-principles/SKILL.md

参考資料

コメント