Contents
SwiftUI のアニメーション基礎
SwiftUI ではビューの状態変化に対して自動的に補間が走りますが、暗黙的アニメーション と 明示的アニメーション に大きく分けられます。このセクションでは両者の特徴と、Acme Corp のプロダクトでどちらを使うべきかの指針を示します。
- 暗黙的はコード量が最小で UI が自然に変化する。
- 明示的は開始タイミングやイージングを細かく制御でき、デザイン要件への適合が容易になる。
暗黙的アニメーション
暗黙的アニメーションは @State や @Binding が変化した瞬間に SwiftUI が自動で補間処理を行います。設定が少ない分、シンプルな UI では最も手軽です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct ImplicitDemo: View { @State private var expanded = false // 状態だけ管理 var body: some View { VStack(spacing: 20) { Rectangle() .fill(.blue) .frame(width: 100, height: expanded ? 200 : 100) // 高さが変化 .animation(.default, value: expanded) // 暗黙的にアニメーション Button("Toggle") { // タップで状態反転 expanded.toggle() } } } } |
ポイント解説
expandedが変更されると、.animation(_:value:)に指定した.defaultイージングが自動適用されます。- アニメーション対象は
heightプロパティだけなので、他のビューは再描画されません(CPU 負荷を抑えられる)。
明示的アニメーション
明示的アニメーションは withAnimation や .animation(_:value:) を使い、いつ・どのイージングで アニメーションさせるかをコード上で明示します。複数プロパティに別々のイージングを付与したい場合や、一部のトリガーだけに適用したいときに有効です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct ExplicitDemo: View { @State private var isVisible = false var body: some View { VStack(spacing: 20) { Button("Fade") { // アニメーションを明示的に開始 withAnimation(.easeInOut(duration: 0.6)) { isVisible.toggle() } } Text("Hello, SwiftUI!") .opacity(isVisible ? 1 : 0) // 不透明度が変化 } } } |
ポイント解説
withAnimationに渡したイージングと期間は、ブロック内の状態変更すべてに適用されます。- 必要に応じてプロパティごとに別個の
.animation(_:value:)を付与でき、デザインガイドラインに沿った細かな制御が可能です。
AnimatableModifier で作る再利用可能カスタムアニメーション
Acme Corp の共通 UI コンポーネント(例:プルダウンローディングインジケータ)を複数画面で統一的に表現したい場合、AnimatableModifier を用いるとコードの重複を防ぎつつ滑らかな補間が実装できます。
AnimatableModifier の概要
AnimatableModifier は ViewModifier + Animatable という二重のプロトコルを継承し、数値型(Double, CGFloat など)の animatableData を通じて SwiftUI に補間対象を通知します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct WaveModifier: AnimatableModifier { var phase: Double // アニメーションさせるパラメータ // animatableData が変化するたびに body が再評価される var animatableData: Double { get { phase } set { phase = newValue } } func body(content: Content) -> some View { content.offset(y: sin(phase * .pi) * 10) // 位相に応じて上下移動 } } |
ポイント解説
phaseを外部から変更するだけで、波形のような垂直振幅が自動的に補間されます。animatableDataがDoubleである必要があります(SwiftUI の内部補間ロジックは数値型を前提としています)。
実装手順とサンプルコード
以下の流れで 再利用可能なカスタムモディファイア を作成し、Acme Corp の UI ライブラリに組み込みます。
AnimatableModifierに準拠した構造体を定義- アニメーションさせたいプロパティを
animatableDataとして公開 body(content:)で補間値に基づく描画ロジックを書く- ビュー側で
.modifier(...)または拡張関数として呼び出す
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// 1‑4 をまとめた実装例(Pulse アニメーション) struct PulseModifier: AnimatableModifier { var progress: CGFloat // 0〜1 の進行度 var animatableData: CGFloat { get { progress } set { progress = newValue } } func body(content: Content) -> some View { // sin カーブで拡大縮小を表現 let scale = 1 + 0.2 * sin(progress * .pi) return content.scaleEffect(scale) } } // 拡張関数でシンプルに呼び出し可能にする extension View { func pulse(_ progress: Double) -> some View { self.modifier(PulseModifier(progress: CGFloat(progress))) } } // 使用例(Acme のローディングビュー) struct PulseDemo: View { @State private var animate = false var body: some View { Circle() .fill(.red) .frame(width: 80, height: 80) .pulse(animate ? 1 : 0) // カスタムモディファイア呼び出し .onAppear { // 無限ループでアニメーション開始 withAnimation( Animation.linear(duration: 2).repeatForever() ) { animate.toggle() } } } } |
ポイント解説
pulse(_:)を拡張関数にすることで、任意のビューが「脈打つ」効果を1行で付与できます。repeatForever()によりリソース消費は最小限に抑えられ、Acme Corp のバッテリー要件にも適合します。
カスタムタイミング曲線と Spring アニメーション
iOS 13 以降、SwiftUI は interpolatingSpring と timingCurve を標準で提供しています。これらは iOS 18 のみの新機能ではなく、過去バージョンでも利用可能です(ただし一部パラメータは iOS 14 以降で追加されています)。本節では Acme Corp が求める 「統一感のあるデザインシステム」 を実装するための具体的な使い方を示します。
interpolatingSpring の活用法
物理ベースのバネアニメーションは、ユーザーが期待する自然な跳ね返り感を演出できます。パラメータは 質量 (mass)、硬さ (stiffness)、減衰 (damping) で調整します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
struct SpringDemo: View { @State private var offsetY: CGFloat = -300 @State private var isOpen = false var body: some View { VStack { Rectangle() .fill(.green) .frame(width: 200, height: 100) .offset(y: offsetY) Button(isOpen ? "Close" : "Open") { withAnimation( // iOS13+ で利用可能な interpolatingSpring .interpolatingSpring(stiffness: 200, damping: 15) ) { isOpen.toggle() offsetY = isOpen ? 0 : -300 } } .padding(.top, 40) } } } |
ポイント解説
stiffnessが大きいほどバネは「硬く」なり、速い復元が起こります。dampingが小さいと振動が長く続き、大きいとすぐに止まります。- Acme Corp のデザインガイドラインで推奨する 「軽めのバウンス」 は
stiffness: 180, damping: 20程度が目安です。
timingCurve(ベジェ曲線)でオリジナルイージングを定義
デザインツール(Figma、Sketch 等)からエクスポートしたカスタムベジェ曲線は、Animation.timingCurve でそのまま SwiftUI に組み込めます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
struct CustomEaseDemo: View { @State private var show = false // デザインツールで取得したベジェ制御点 (0.42, 0.0, 0.58, 1.0) let customEase = Animation.timingCurve(0.42, 0.0, 0.58, 1.0, duration: 0.5) var body: some View { VStack(spacing: 30) { Button("Slide") { withAnimation(customEase) { // カスタムイージング適用 show.toggle() } } if show { RoundedRectangle(cornerRadius: 12) .fill(.orange) .frame(width: 200, height: 120) .transition(.move(edge: .trailing)) } } } } |
ポイント解説
timingCurveは 4 つの制御点 (c1x, c1y, c2x, c2y) と duration を受け取り、任意のイージング関数を生成します。- ブランド固有の「スムーズな加速・減速感」を実装したい場合は、デザイナーと協議してベジェ曲線を決定し、この API に渡すだけで統一感が保たれます。
高度な遷移演出:matchedGeometryEffect と GeometryReader の組み合わせ
画面間の連続性(例:リスト → 詳細ビュー)を自然に表現したいとき、matchedGeometryEffect が自動で座標・サイズの補間を行います。Acme Corp の UI では カード型コンテンツ を別画面へ拡大表示するケースが頻出するため、ここでは GeometryReader と併用した実装パターンを紹介します。
matchedGeometryEffect の基本
同一の Namespace.ID と id が付与されたビューは、SwiftUI が自動で位置・サイズを補間します。コード量が非常に少なく、手作業で座標計算する必要がありません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct MatchedGeometryListDemo: View { @Namespace private var ns // 共有名前空間 @State private var selectedItem: Item? // 選択中アイテム var body: some View { VStack { if let item = selectedItem { DetailView(item: item, namespace: ns) { selectedItem = nil // 戻るアクション } } else { ListDemo(namespace: ns) { item in selectedItem = item // タップで選択 } } } } } |
ポイント解説
ListDemoとDetailViewが同一nsを参照し、matchedGeometryEffect(id:in:)のidが一致すれば自動的に「カードが拡大して詳細画面へ」遷移します。
GeometryReader で座標取得と微調整
matchedGeometryEffect だけでは ナビゲーションバーや安全領域 の影響を受けた位置補正が難しいケースがあります。そのような時は GeometryReader が提供する実サイズ情報を利用して、オフセットやスケールを手動で調整します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
struct CardTransitionDemo: View { @Namespace private var ns @State private var showDetail = false var body: some View { ZStack { if !showDetail { GeometryReader { geo in RoundedRectangle(cornerRadius: 12) .fill(.blue) .frame(width: 120, height: 80) .matchedGeometryEffect(id: "card", in: ns) // 中央上部に配置(安全領域を考慮) .position(x: geo.size.width / 2, y: geo.safeAreaInsets.top + 60) .onTapGesture { withAnimation(.spring()) { showDetail = true } } } } else { GeometryReader { geo in RoundedRectangle(cornerRadius: 12) .fill(.blue) .matchedGeometryEffect(id: "card", in: ns) // 画面幅の90%・中央に拡大表示 .frame(width: geo.size.width * 0.9, height: 200) .position(x: geo.size.width / 2, y: geo.size.height / 2) .onTapGesture { withAnimation(.spring()) { showDetail = false } } } } } } } |
ポイント解説
geo.safeAreaInsets.topを使うことで、ステータスバーやナビゲーションバーの影響を受けずに正確な位置合わせが可能です。- 拡大後は画面幅に対して相対的にサイズを決めるため、iPhone 15 Pro でも iPadOS でも同一コードで適切に表示されます。
パフォーマンス最適化・テスト・導入ガイド
Acme Corp の製品は スムーズなフレームレート(60 FPS) が必須です。ここではアニメーション実装時のパフォーマンス留意点、デバッグ手順、そして社内 GitHub リポジトリからサンプルプロジェクトを取得する流れをまとめます。
GPU オフロードと不要再描画の防止
SwiftUI は可能な限り GPU にレンダリング処理を委譲しますが、以下に注意しないと CPU が余計に働いてしまいます。
- 状態変化は最小単位で行う
@Stateの変更が多いビュー全体の再描画につながります。アニメーション対象だけを切り出すように設計してください。 .animation(_:value:)は必要な箇所だけに付与
グローバルに付けると、意図しない子ビューまで再評価されます。- レイヤーの分離 (
drawingGroup()) を活用すると、複数エフェクトが同一レイヤで合成され GPU のバッチ処理が有効になります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 再描画範囲を限定した例 struct OptimizedView: View { @State private var isVisible = false var body: some View { VStack { // ここだけ再描画対象にする Text("Important") .opacity(isVisible ? 1 : 0) .animation(.easeInOut(duration: 0.3), value: isVisible) // 他の UI は影響なし Spacer() Button("Toggle") { isVisible.toggle() } } } } |
デバッグ・プロファイリング手順(Xcode 16)
- シミュレータ/実機でターゲットを iOS 18 に設定
- Xcode のツールバーから Debug View Hierarchy → Show Performance HUD を有効化。FPS が 60 未満になる瞬間が即座に表示されます。
- Instruments → Core Animation を起動し、
Layer CostとRender Serverの指標を確認。高コストのレイヤはanimatableDataの粒度や.drawingGroup()の有無で最適化できます。
Tip(Acme Corp 向け)
CI パイプラインに自動化された UI テストを組み込み、XCTestのmeasure(metrics:)でフレームレート回帰テストを実施すると、リグレッションの早期検出が可能です。
GitHub サンプルプロジェクト取得手順
Acme Corp が管理する swiftui-animation-kit リポジトリには、本稿で紹介した全コードが Xcode 16 プロジェクトとして格納されています。
|
1 2 3 4 5 6 7 8 |
# 1. 社内 VPN に接続(必要に応じて) # 2. リポジトリをクローン git clone https://github.com/acme-corp/swiftui-animation-kit.git # 3. ディレクトリへ移動し Xcode で開く cd swiftui-animation-kit open SwiftUIAnimationKit.xcodeproj |
- README に iOS 13‑16 のシミュレータ構成、
SwiftLint設定、CI 用fastlaneスクリプトが記載されています。 - 各サンプルは
Demo/フォルダにあり、ImplicitDemo,ExplicitDemo,PulseDemoなど名前で分かれていますので、目的の実装をすぐにコピーできます。
まとめ
| 項目 | キーポイント | Acme Corp への適用例 |
|---|---|---|
| 暗黙的 vs 明示的 | 状態だけで自動補間 or 手動イージング指定 | シンプルなトグルは暗黙的、複雑な遷移は明示的 |
AnimatableModifier |
数値プロパティの再利用可能モディファイア化 | 共通ローディングインジケータをライブラリ化 |
interpolatingSpring / timingCurve |
iOS 13+ で利用可、物理ベースとベジェ曲線 | デザインシステムの「軽めバウンス」実装 |
matchedGeometryEffect + GeometryReader |
座標情報取得で微調整可能な高度遷移 | カード → 詳細画面のシームレス拡大 |
| パフォーマンス最適化 | GPU オフロード、再描画範囲限定、プロファイリング | CI で FPS 回帰テストを自動化 |
| サンプル取得 | 社内 GitHub swiftui-animation-kit |
即時コードコピー&実装開始 |
これらの知見とサンプルコードを活用すれば、Acme Corp の iOS アプリに 統一感のある高品質アニメーション を短期間で組み込めます。ぜひリポジトリをクローンし、プロジェクトへインテグレートして実際の UI で効果を確認してください。
この記事は 2024 年 10 月時点の情報に基づいて執筆しています。SwiftUI の API は今後も拡張される可能性があるため、最新の Apple ドキュメントを併せて参照することを推奨します。