axiom-swiftui-animation-ref について
SwiftUI のアニメーションを学び、実装する開発者向けのスキルです。補間、アニメーションの種類、トランザクションに加え、iOS 26 の `@Animatable` マクロやカスタム実装の性能指針も扱います。
主な機能
- 01Animatable と VectorArithmetic の基礎
- 02iOS 26 の `@Animatable` マクロ
- 03タイミングカーブとスプリングアニメーション
- 04スコープ付きアニメーションとトランザクション
- 05カスタムアニメーションアルゴリズム
- 06カスタム適合の性能に関する指針
活用シーン
- 01カスタムシェイプやレイアウトをアニメーション化
- 02スプリングとタイミングカーブの挙動を選択
- 03特定のビュー属性だけにアニメーションを適用
- 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、RoundedRectangleCapsule、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)
仕組み
numberが 0 から 100 に変化します- SwiftUI はアニメーションの各フレームで
bodyを呼び出します - 各フレームで新しい
numberの値を取得します:0 → 5 → 15 → 30 → 55 → 80 → 100 - テキストが更新され、補間された整数が表示されます
パフォーマンスに関する警告
カスタムの Animatable 準拠は負荷が高くなります — SwiftUI はメインスレッドで各フレームごとに body を呼び出します。組み込みのエフェクト(.scaleEffect()、.opacity())はメインスレッド以外で実行され、body を呼び出しません。組み込みのモディファイアでは実現できないエフェクト(たとえば、各フレームでサブビューの位置を変更するカスタム Layout のアニメーション)に限り、カスタム準拠を使用してください。
パート 3:@Animatable マクロ(iOS 26 以降)
概要
@Animatable マクロを使うと、Animatable プロトコルに手動で準拠させるための定型コードをなくせます。
iOS 26 より前は、次の作業が必要でした。
Animatableに手動で準拠させるanimatableDataのゲッターとセッターを記述する- 複数のプロパティには
AnimatablePairを使用する - アニメーション対象外のプロパティを手動で除外する
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
}
}
仕組み
withAnimationがトランザクションを開始します- トランザクションの辞書にアニメーションを設定します
- クロージャーを実行します(状態の変更)
- トランザクションがビュー階層を下へ伝播します
- アニメーション可能な属性がアニメーションを確認し、値を補間します
明示的なアニメーション
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
}
}
}
仕組み
- SwiftUIは現在の
Transactionにアニメーション情報を保存します context.animate()がTransactionのアニメーションを読み取ります- そのアニメーションをクロージャー内のUIViewの変更に適用します
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は累積した速度を引き継いで滑らかに減速します。
トラブルシューティング
プロパティがアニメーション化されない
次の順に確認してください。
- 型はVectorArithmeticに準拠していますか? —
Intはアニメーション化できません。DoubleまたはFloatを使用してください - アニメーション修飾子がありますか? —
.animation(.spring, value: x)またはwithAnimationが必要です - 正しい値を追跡していますか? —
.animation(.spring, value: progress)を使い、.animation(.spring, value: title)は使わないでください - ビューは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