ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIの高度なアニメーションの世界
SwiftUIの最新アップデートでアニメーションを進化させましょう。複数のステップを構築し、構成されたマルチトラックアニメーション効果をキーフレームを使用して追加し、ユニークな方法でAPIを組み合わせて、あなたのアプリに命を吹き込みましょう。
関連する章
- 0:00 - Introduction
- 2:23 - Animation phases
- 8:12 - Keyframes
- 15:07 - Tips and tricks
リソース
関連ビデオ
WWDC23
-
ダウンロード
♪ ♪
こんにちは SwiftUIの高度なアニメーションの 世界にようこそ 私はSwiftUIチームのTimです SwiftUIには あなたのアプリを輝かせる 強力なアニメーションツールがあります インタラプトが可能で 物理ベースで 信憑性のある動きのアニメーションが フレームワーク全体に 深く統合されています 今日はアプリのアニメーションを 更に改善させる エキサイティングな新ツールを紹介します 始める前にSwiftUIの既存の アニメーションツールを復習しましょう 他の動画で好きなペットに投票できる このアプリを見たことがあるかもしれません このデモを簡単にするために ベストな選択肢である猫以外を 削除しておきました アニメーションの追加は簡単で withAnimation か animation modifierを追加するだけです アプリのステート変更後に SwiftUIは前のステートから現状へ 補間するアニメーションを適用します しかし アニメーションでは 人生と同じように 思いがけないときに 素晴らしい経験が見つかることがあります 時にはありきたりな道から外れて 特別なものを作るために 旅そのものに集中するのです アニメーションは単純に前のステートから 次へ進むだけではないこともあります 今日は 複雑な マルチステップアニメーションのための 強力な新しいツールを紹介します 2つのステート間で アニメーションするのではなく これらのアニメーションは 順番に起こる複数のステップを定義できます これらは2つの状況で特に便利です ビューが表示されている間 ずっとループする連続アニメーション… イベント発生時にパルスするビューのような イベント駆動型アニメーションです 本動画ではこういった アニメーションの構築を より簡単にする 新しいAPIファミリーを紹介します まずanimation phasesを紹介します これによりSwiftUIは アニメーションを構成する 計画済みの一連の状態を 自動で進められます 次にkeyframeを使ってアニメーションを さらに進化させる方法を紹介します 最後にこのAPIを最大限に活用するための 高度なヒントとコツを紹介します 準備は整いましたね さっそく飛び込みましょう 私はSwiftを書いていないときは トレイルランニングをするのが好きです トレイルレースはとても長いです ウルトラマラソンは完走するのに 丸一日あるいは数日かかるので 私は今後のイベントの計画を立てたり ランニング中の情報管理に役立つアプリを 作ってきました トレイルでは栄養補給が超重要です 残念なことにレース後半疲れが溜まると 食べるのを忘れてしまいがちです そこで 適切なタイミングで 食事のリマインドをする機能を追加しました 画面下のリマインダーが 食事の時間が 過ぎていると知らせています しかし問題があります レース後半になって疲れてくると 控えめな指標は 見逃してしまうことがあるのです うっかり食事を抜くのは避けたいので リマインダーを目立たせる動きを加えます このビューを見てみましょう アニメーションのハイライト効果をつけて より目立たせます アニメーションさせるには .phaseAnimator modifierを適用します .phaseAnimator modifierは マルチパートアニメーションの 各ステップを定義する ステートシーケンスを提供します SwiftUIはこれらのステートの間で 自動的にアニメーションを行います この場合 2つの状態の間を アニメーションさせるだけです ハイライト付きとハイライト無しです ですので単純にブール値を使います 次に 現在のphaseに応じて ビューの外観を変更するために いくつかのmodifierを適用します まずopacity modifierから始めます ハイライトされているときは ビューを完全に不透明にし そうでないときは50%透明にします ビューはすぐにアニメーションを開始します SwiftUIがあなたの代わりに 何をしているか説明しましょう ビューではphase animator modifierに 2つのphaseを与えました falseとtrueです ビューの最初の表示では 最初のphaseがアクティブになり ビューが50%透明になります SwiftUIはすぐに ビューが 完全に不透明になる 次の段階へのアニメーション遷移を 開始します そしてそのアニメーションが終わると SwiftUIは再び進みます 2つのフェーズしかないので 最初にループします これによってアニメーションは 2つの状態の間を循環します もちろん 2つ以上のフェーズを含む アニメーションを定義することも ビューmodifierを追加することもできます 後ほどお見せします さてビューがアニメーションしている間 その効果は実に控えめです 不透明度ではなく 前景のスタイルを変えてみましょう ハイライトされているときは赤を使い それ以外は初期のスタイルに戻します アニメーションは少し唐突ですが より見やすくなりました デフォルトでは SwiftUIは springアニメーションを使用します springは動的なステート変化の 扱いに最適ですが ここではよりスムーズで 一貫性のあるアニメーションが必要です 末尾にanimationクロージャを 追加することで変更できます 各phaseで異なるアニメーションを 使う場合に備えて アニメーションの対象となる phaseが渡されます しかし私はいつも同じ イーズインアウトアニメーションを 速度を落としたカスタムのdurationで 使いたいのです 通常 インタラクティブなステート変更に 1秒間のアニメーションを使わないでしょう アニメーションが終わるのを 待たせたくないからです しかし 今回は アンビエント効果を構築しているので 少しゆっくりした動きでも構いません 食事が遅れたときの私のペースのようにね 栄養補給という緊急の問題を解決できたので AnimationPhaseの使い方を もうひとつ見てみましょう イベントによってトリガーされる アニメーションです 私はしばらくの間 このアプリに取り組んできて 友人がどのレースを走ったかを 見る機能を追加しました 絵文字は他の人が残したリアクションです ランナーは誰でも自問することがあります: なぜ走るのか? なぜこんな長距離を?と それに対してこのアプリができることは 誰かがレースを高評価したときに 楽しい効果を追加して 承認欲求を満たすことです 誰かがリアクションするたびに 再生されるアニメーションを追加します まずはアニメーションのphaseを定義します 単純に2つの状態を 交互に繰り返す前の例とは異なり より複雑なアニメーションが欲しいですね enumはアニメーションのステップのリストを 定義するのに最適です ここでは 3つのcaseを追加しました: 最初の出現のcase ビューを上に移動するcase ビューを拡大するcaseです ビュー本体を単純化するため 計算プロパティをenumに追加して 適用する効果を定義します ビューを上にジャンプさせたいので 計算された垂直オフセットプロパティを 追加しました enumを切り替えて 各caseに適したオフセットを返します 同様にビューのスケールと 前景スタイルを決定するため 2つの計算プロパティを追加しました ここでは実装をお見せしませんが これらも垂直オフセットプロパティと同様に switch文を使用しています ビューに戻って アニメーションを追加しましょう phaseAnimator modifierを追加しますが 今回はtrigger値を指定します trigger値を得たphaseAnimator modifierは 指定した値の変化を観察します 変化が発生すると 指定したphaseアニメーションを開始します phaseタイプで定義した 計算プロパティを用いて ビューにmodifierを適用します このアニメーションは 技術的には正しいですが 出来がよくありません 少しもたつきます springアニメーションなどを使って トランジションごとに アニメーションをカスタマイズしましょう ずっと良くなりました! さらに進化させたいなら どうすれば良いでしょう? 誰かがトレイルで50マイルや 100マイルを走り終えたとき その人を褒め称えるアニメーションを作って 走った甲斐があったと思ってもらいましょう 更なるコントロールが必要な場合は keyframeという強力なツールがあります 次は keyframeを使って タイミングと動きを完全にコントロールして 複雑で協調的なアニメーションを 定義する方法を紹介します まず keyframeと phaseの違いについて説明します phaseはビューに一個づつ与えられる 個別のstateを定義します そして SwiftUIは 既にご存知の アニメーションタイプを使用して これらのstate間で アニメーションを行います 個別stateとしてモデル化できる アニメーションのときに効果的で state遷移が起きるとき すべてのプロパティは 同時にアニメーションされます アニメーションが終了すると SwiftUIは次のstateをアニメーションします これはアニメーションの 全phaseで継続します プロパティを別々に アニメートさせる場合はどうすれば? そこでkeyframeが登場します keyframeを使えば特定のタイミングで 値を定義することができます 例として回転エフェクトで このビューをアニメートしてみましょう このドットはkeyframeを表し 各ポイントで使用する角度を示しています アニメーション再生時に SwiftUIはこれらkeyframe間の値を補間し ビューにmodifierを適用するために使えます またkeyframeは個々のタイミングで 別々のトラックを定義することで 同時に複数のエフェクトを 個別にアニメートできます SwiftUIのどんなmodifierも keyframeを使って動かせるので これは本当に強力です この例ではkeyframeを使って 垂直ストレッチやスケール 平行移動などのトラックを動かしています ビューに戻って コードでどのように 見えるか見てみましょう 作りたいアニメーションの アイデアはすでにあるので まずはアニメーションを動かす プロパティを定義します そのために独立してアニメートされる さまざまなプロパティをすべて含む 新しい構造体を作成します keyframeはAnimatableプロトコルに準拠した 任意の値をアニメーション化できます Doubleを使用しているプロパティは 今はAnimatableに準拠しています phaseは別々の個別のstateを モデル化しますが keyframeは指定した型の 補間値を生成します アニメーションの進行中 SwiftUIはビューの更新用に 全フレームでこのタイプの値を提供します 次にkeyframeAnimator modifierを 追加します このmodifierは以前に使用した phaseAnimatorに似ていますが keyframeを受け入れます 構造体のインスタンスを 初期値として指定しています 定義したkeyframeは この値にアニメーションを適用します 次に構造体の各プロパティに対して ビューにmodifierを適用します そして最後にkeyframeを定義します 前述のように keyframeを使うと プロパティごとに異なるkeyframeを使った 高度なアニメーションを作成できます これを可能にするために keyframeはトラックで構成されます 各トラックはアニメートするタイプの 異なるプロパティを制御します このプロパティはトラックの作成時に 指定するKeyPathによって指定されます ここではscaleプロパティの keyframeを追加します 最初にlinearKeyframeを追加し スケールの初期値を繰り返し 0.36秒間保持します どうやって0.36に決めたかというと アニメーションの感触を変えるために いろいろな値を試して その持続時間を見つけました keyframeではこれが大切です アプリに合ったアニメーションを作るには 検証が大切です Xcodeのプレビューは アニメーションの微調整に最適です 次はSpringKeyframeを追加します ターゲットに向かって値を引っ張るために spring関数が使われます そしてdurationも指定します durationが設定された SpringKeyframeでは spring関数がそのdurationの間だけ 値をアニメートします その後 次のkeyframeへの 補間が始まります 最後にスケールを1.0に戻す 別のSpringKeyframeを追加します keyframeの種類によって 値の補間方法が変わります ここまでLinearKeyframeと SpringKeyframeを見てきました 実はkeyframeには4つの種類があります 違いを説明します LinearKeyframeは直前のkeyframeから ベクトル空間で直線的に補間します SpringKeyframeはその名の通り バネ関数を使用して 前のkeyframeからターゲット値に補間します CubicKeyframeは3次ベジエ曲線を使って keyframe間を補間します 複数の3次keyframeを順番に組み 合わせると Catmull-Romスプラインと同じ曲線になります 最後にMoveKeyframeは 補間なしで即座に値にジャンプします それぞれのkeyframeは 完全なカスタマイズに対応し アニメーション内で異なる種類の keyframeを混合することができます SwiftUIはアニメーションの 連続性を保つために keyframe間の速度を維持します ビューでは次のトラックを追加する 準備ができました ここでは垂直方向の移動を アニメーション化するために linearとspringのkeyframeを使用しました ジャンプアップ直前にビューは 予期して引き戻されます これをspringKeyframeでモデリングし ビューが上に移動する前に短く引き下げます いい感じですがアニメートする プロパティがまだ2つあります 垂直ストレッチと回転です 立方体のkeyframeを使った 垂直方向のストレッチから始めます 繰り返しますが試行錯誤が大切です keyframeを使った さまざまなアニメーション方法を 試してみてください squashとstretchはアニメーションに 大きなエネルギーを与えます 回転もアニメーション化しましょう 素晴らしい 先ほど見た曲線は? あれは今作ったアニメーションを 視覚化したものです SwiftUI modifierの適用に トラックの追加もできます いろいろな組み合わせを試すのは とても楽しいです keyframeモデルについて 少し復習しましょう keyframeは定義済みのアニメーションです つまり UIが流動的で インタラクティブであるべき状況では keyframeは通常のSwiftUIの アニメーションの代わりにはなりません keyframeは再生可能な ビデオクリップのように考えてください 多くの操作性を与えますが トレードオフがあります アニメーションの進行方法を 正確に指定するため keyframeアニメーションはspringのように 優雅にリターゲットできません そのためアニメーション途中の keyframe変更は避けましょう keyframeは定義したタイプの値を アニメートするので それを使うことで ビューに各種modifierを適用します 1つのkeyframeトラックを使って 1つや組み合わさったmodifierを 動かすこともできます あなた次第です アニメーションは 定義した値で行われるため すべてのフレームで更新されます つまり keyframeアニメーションを ビューに適用している間は 高価な操作は避けるべきです 最後にkeyframeで 他に何ができるか説明します 私のアプリにはルートを示す レースマップが含まれています 自動的にズームインして コースをたどるアニメーションを 追加しましょう ありがたいことにMapKitではkeyframeで カメラを動かせるようになりました ここではMapビューでコースを表示しています このビューにはすでにルートがあり あるレースのルートの すべての座標を含むモデルです ツアーを構築するために stateプロパティと 変更用ボタンを追加します 最後に使うのは新しい mapCameraKeyframeAnimator modifierです トリガー値を与えKeyframeを追加します 先ほどのハートアイコンと同様です トリガー値が変わるたびにマップは これらのKeyframeを使ってアニメーションします Keyframeの最終値は アニメーションの最後に 使われるカメラの値を決定します 最後にボタンを押すとツアーが始まります アニメーション中に ユーザーがジェスチャーを行うと アニメーションは解除され ユーザーは カメラを完全にコントロールできます 中心座標 方位 距離を 個別にアニメーションさせることで コースをスムーズにアニメーションさせ ズームアウトして 鳥瞰図に戻すことができます 最後に keyframeを手動で評価することで あらゆるエフェクトを使う方法を紹介します ここまでkeyframeAnimator modifierを 見てきました modifierの外では KeyframeTimelineタイプを使って keyframeとトラックのセットを 捉えることができます 初期値のこの型と アニメーションを定義する keyframeトラックを初期化します ビューmodifierと同じようにです KeyframeTimelineが提供するAPIは 最長トラックのdurationと 同等のdurationを与えます またアニメーション範囲内の 任意の時間の値の計算ができます これは先ほどお見せした曲線図のような keyframeを使用するSwift Chartsの 視覚化を容易にします keyframeで定義された曲線を 好きなように使ったり keyframeを他のAPIと創造的に 組み合わせることも可能です 例えば GeometryProxyと組み合わせて スクロール位置を使ってkeyframe駆動の エフェクトをスクラブしたり TimelineViewを使って時間に基づいて 更新もできます 使い方がわからなくても大丈夫 これは高度なツールであり ほとんどのデベロッパは ビューmodifierにとどまるでしょう 材料としてはあると覚えておいてください あなたがどのような クリエイティブな方法で使うか楽しみです 以上でこの旅は終わりです 新しいAPI機能を 楽しんでもらえることを楽しみにしてます ぜひ次のことを覚えてください 連鎖するアニメーションにはphaseを使う 既存のアニメーションタイプを すべて使うので すぐに使い始めることができます 完全制御が必要な複雑なアニメーションには keyframeを使いましょう そして最後に いろいろな試行錯誤を楽しんでください アニメーションはエキサイティングです これらのツールがあなたやあなたのアプリを 新しい世界へと導びきますように ありがとう! ♪ ♪
-
-
0:42 - Scale Animation
struct Avatar: View { var petImage: Image @State private var selected: Bool = false var body: some View { petImage .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { withAnimation { selected.toggle() } } } }
-
3:13 - Boolean Phases
OverdueReminderView() .phaseAnimator([false, true]) { content, value in content .foregroundStyle(value ? .red : .primary) } animation: { _ in .easeInOut(duration: 1.0) }
-
6:20 - Custom Phases
ReactionView() .phaseAnimator( Phase.allCases, trigger: reactionCount ) { content, phase in content .scaleEffect(phase.scale) .offset(y: phase.verticalOffset) } animation: { phase in switch phase { case .initial: .smooth case .move: .easeInOut(duration: 0.3) case .scale: .spring( duration: 0.3, bounce: 0.7) } } enum Phase: CaseIterable { case initial case move case scale var verticalOffset: Double { switch self { case .initial: 0 case .move, .scale: -64 } } var scale: Double { switch self { case .initial: 1.0 case .move: 1.1 case .scale: 1.8 } } }
-
9:48 - Keyframes
ReactionView() .keyframeAnimator(initialValue: AnimationValues()) { content, value in content .foregroundStyle(.red) .rotationEffect(value.angle) .scaleEffect(value.scale) .scaleEffect(y: value.verticalStretch) .offset(y: value.verticalTranslation) } keyframes: { _ in KeyframeTrack(\.angle) { CubicKeyframe(.zero, duration: 0.58) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.degrees(-16), duration: 0.125) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.zero, duration: 0.125) } KeyframeTrack(\.verticalStretch) { CubicKeyframe(1.0, duration: 0.1) CubicKeyframe(0.6, duration: 0.15) CubicKeyframe(1.5, duration: 0.1) CubicKeyframe(1.05, duration: 0.15) CubicKeyframe(1.0, duration: 0.88) CubicKeyframe(0.8, duration: 0.1) CubicKeyframe(1.04, duration: 0.4) CubicKeyframe(1.0, duration: 0.22) } KeyframeTrack(\.scale) { LinearKeyframe(1.0, duration: 0.36) SpringKeyframe(1.5, duration: 0.8, spring: .bouncy) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.verticalTranslation) { LinearKeyframe(0.0, duration: 0.1) SpringKeyframe(20.0, duration: 0.15, spring: .bouncy) SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy) SpringKeyframe(0.0, spring: .bouncy) } } struct AnimationValues { var scale = 1.0 var verticalStretch = 1.0 var verticalTranslation = 0.0 var angle = Angle.zero }
-
15:22 - Map Keyframes
struct RaceMap: View { let route: Route @State private var trigger = false var body: some View { Map(initialPosition: .rect(route.rect)) { MapPolyline(coordinates: route.coordinates) .stroke(.orange, lineWidth: 4.0) Marker("Start", coordinate: route.start) .tint(.green) Marker("End", coordinate: route.end) .tint(.red) } .toolbar { Button("Tour") { trigger.toggle() } } .mapCameraKeyframeAnimation(trigger: playTrigger) { initialCamera in KeyframeTrack(\MapCamera.centerCoordinate) { let points = route.points for point in points { CubicKeyframe(point.coordinate, duration: 16.0 / Double(points.count)) } CubicKeyframe(initialCamera.centerCoordinate, duration: 4.0) } KeyframeTrack(\.heading) { CubicKeyframe(heading(from: route.start.coordinate, to: route.end.coordinate), duration: 6.0) CubicKeyframe(heading(from: route.end.coordinate, to: route.end.coordinate), duration: 8.0) CubicKeyframe(initialCamera.heading, duration: 6.0) } KeyframeTrack(\.distance) { CubicKeyframe(24000, duration: 4) CubicKeyframe(18000, duration: 12) CubicKeyframe(initialCamera.distance, duration: 4) } } } }
-
16:26 - KeyframeTimeline
// Keyframes let myKeyframes = KeyframeTimeline(initialValue: CGPoint.zero) { KeyframeTrack(\.x) {...} KeyframeTrack(\.y) {...} } // Duration in seconds let duration: TimeInterval = myKeyframes.duration // Value for time let value = myKeyframes.value(time: 1.2)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。