swiftui-animation について
SwiftUI 開発者向けに、アニメーションのレビュー、作成、修正を支援するスキルです。最新の API、タイミング、トランジション、アクセシビリティへの配慮を扱い、「視差効果を減らす」設定への対応も確認します。
主な機能
- 01アニメーション API の選択
- 02状態駆動・フェーズアニメーション
- 03キーフレームと共有要素の遷移
- 04SF Symbols の効果と描画
- 05「視差効果を減らす」の確認
活用シーン
- 01状態変化にアニメーションを追加
- 02ビューの挿入・削除を演出
- 03アニメーション範囲や性能の問題を調査
- 04「視差効果を減らす」への対応を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-animationインストール後、Claude Code で/swiftui-animationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/animation-advanced.mdcore-animation-bridge.md詳細
SwiftUI アニメーション(iOS 26以降)
SwiftUI アニメーションをレビュー、作成、修正します。Swift 6.3 のパターンを使用し、適切なタイミング、トランジション、アクセシビリティへの対応を備えた最新のアニメーション API を適用します。
目次
- トリアージワークフロー
- withAnimation(明示的なアニメーション)
- 暗黙的なアニメーション
- Spring 型(iOS 17以降)
- PhaseAnimator(iOS 17以降)
- KeyframeAnimator(iOS 17以降)
@Animatable Macro- matchedGeometryEffect(iOS 14以降)
- ナビゲーションズームトランジション(iOS 18以降)
- トランジション(iOS 17以降)
- ContentTransition(iOS 16以降)
- シンボルエフェクト(iOS 17以降)
- シンボルの描画モード
- よくある間違い
- レビュー用チェックリスト
- 参考資料
トリアージワークフロー
ステップ 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 を参照してください。