WWDC Quick Look 💓 By SwiftGGTeam
Animate symbols in your app

Animate symbols in your app

元の動画を見る

ハイライト

iOS 17 と macOS Sonoma では Symbols framework が導入され、カスタムシンボルを含むすべての SF Symbols に統一されたアニメーション API が提供されます。開発者は .symbolEffect() 修飾子とドット区切りのエフェクト名を使って、SwiftUI、UIKit、AppKit に Bounce、Pulse、Variable Color などのアニメーション効果を追加できます。

主要内容

シンボルアニメーションの 4 つの動作

以前はアイコンを動かすために UIView.animatewithAnimation を手書きし、キーフレームも自分で計算する必要がありました。Symbols framework はこれを簡単にし、必要な効果を宣言するだけで、アニメーションの詳細はシステムが処理します。

すべての効果は 4 種類の動作に分類できます(04:25):

動作説明代表的な効果
Discrete(離散)一度だけ再生され、完了後に自動終了するアニメーションBounce
Indefinite(継続)シンボルの状態を変更して維持し、明示的な削除が必要Scale、Variable Color
Transition(遷移)シンボルの表示と非表示を制御Appear、Disappear
Content Transition(コンテンツ遷移)2 つのシンボル間を切り替えるReplace

各動作は Swift protocol に対応しています。効果はこれらの protocol に準拠することで、どの動作をサポートするかを宣言します。たとえば Variable Color は Indefinite と Discrete の両方をサポートし、実際の動作は API の使い方で決まります。

ドット区切りの効果名

Symbols framework の設計上の特徴のひとつは、効果名にドット区切りの構文を使うことです(02:03):

.bounce
.variableColor.iterative.reversing
.scale.up

これらは文字列ではなく実際の Swift コードです。Xcode は各部分を自動補完し、設定ミスはコンパイル時に検出できます。SF Symbols app の Animation タブで効果をプレビューし、その効果名をそのままコードへコピーできます。

SwiftUI で使う

SwiftUI には symbolEffect 修飾子が追加されました(05:11)。もっとも簡単な使い方は、Image に直接効果を追加することです:

Image(systemName: "wifi.router")
    .symbolEffect(.variableColor.iterative.reversing)
    .symbolEffect(.scale.up)

効果は重ねて適用できます。上のコードでは Variable Color アニメーションと拡大効果を同時に適用しています。

UIKit/AppKit で使う

UIKit 和 AppKit 的 UIImageView/NSImageView 新增了 addSymbolEffect 方法(05:30):

let imageView: UIImageView = ...
imageView.addSymbolEffect(.variableColor.iterative.reversing)
imageView.addSymbolEffect(.scale.up)

詳細

Indefinite 効果:アニメーションの開始と停止を制御する

Indefinite 効果は削除されるまで継続して再生されます。SwiftUI では isActive パラメータで制御します(06:49):

struct ContentView: View {
    @State var isConnectingToInternet: Bool = true
    
    var body: some View {
        Image(systemName: "wifi.router")
            .symbolEffect(
                .variableColor.iterative.reversing,
                isActive: isConnectingToInternet
            )
    }
}

キーポイント:

  • isActivetrue のときにアニメーションが開始される
  • isActivefalse になるとアニメーションがなめらかに終了する
  • ネットワーク接続やファイルアップロードなど、「進行中」の状態表示に適している

UIKit/AppKit では removeSymbolEffect(ofType:) でアニメーションを停止します(07:09):

imageView.addSymbolEffect(.variableColor.iterative.reversing)
// 接続に成功したらアニメーションを停止
imageView.removeSymbolEffect(ofType: .variableColor)

Discrete 効果:イベントに応答する一度だけのアニメーション

Discrete 効果は値が変化したときに一度だけ発火します。Bounce は代表的な Discrete 効果です(08:26):

struct ContentView: View {
    @State var bounceValue: Int = 0
    
    var body: some View {
        VStack {
            Image(systemName: "antenna.radiowaves.left.and.right")
                .symbolEffect(
                    .bounce,
                    options: .repeat(2),
                    value: bounceValue
                )
            
            Button("Animate") {
                bounceValue += 1
            }
        }
    }
}

キーポイント:

  • value パラメータが変化したときにアニメーションが発火する
  • options: .repeat(2) によりアニメーションを 2 回繰り返す
  • ボタンをタップした後のフィードバックなど、ユーザー操作への応答に適している

Pulse と Variable Color も Discrete 動作をサポートしています。value で発火させると、一度だけのアニメーションシーケンスとして再生されます。

Replace 効果:シンボル切り替えアニメーション

Replace 効果は、2 つのシンボル間の切り替えにアニメーション遷移を加えます(09:40):

struct ContentView: View {
    @State var isPaused: Bool = false
    
    var body: some View {
        Button {
            isPaused.toggle()
        } label: {
            Image(systemName: isPaused ? "pause.fill" : "play.fill")
                .contentTransition(.symbolEffect(.replace.offUp))
        }
    }
}

キーポイント:

  • contentTransition(.symbolEffect(.replace.offUp)) は、シンボル切り替え時に上方向へ抜ける置換アニメーションを付ける
  • .offUp.offDown などのオプションで置換方向を制御する
  • 再生/一時停止、お気に入り/解除などの状態切り替えに適している

UIKit では setSymbolImage(_:contentTransition:) で同じ効果を実現します(09:57):

let pauseImage = UIImage(systemName: "pause.fill")!
imageView.setSymbolImage(pauseImage, contentTransition: .replace.offUp)

Appear と Disappear:2 つの使い方

Appear と Disappear は 2 種類の動作モードをサポートします(10:15):

方法 1:Indefinite 動作(レイアウトを変更しない)

シンボルはその場で消えたり現れたりし、周囲のビューのレイアウトは変わりません:

Image(systemName: "moon.stars")
    .symbolEffect(.disappear, isActive: isMoonHidden)

方法 2:Transition 動作(レイアウトを変更する)

シンボルがビュー階層に挿入または削除され、周囲のビューが再レイアウトされます:

if !isMoonHidden {
    Image(systemName: "moon.stars")
        .transition(.symbolEffect(.disappear.down))
}

キーポイント:

  • Indefinite 方式はバッジや状態マークの表示/非表示に適している
  • Transition 方式は条件付きで描画するビューに適している
  • .automatic 遷移効果は最適なアニメーションを自動選択する

UIKit では completion handler を使い、アニメーション終了後にビューを削除できます(12:59):

imageView.addSymbolEffect(.disappear) { context in
    if let imageView = context.sender as? UIImageView, context.isFinished {
        imageView.removeFromSuperview()
    }
}

効果の伝播と無効化

SwiftUI では symbolEffect 修飾子が下方向に伝播します(14:19):

VStack {
    Image(systemName: "figure.walk")
        .symbolEffectsRemoved()
    Image(systemName: "car")
    Image(systemName: "tram")
}
.symbolEffect(.pulse)

キーポイント:

  • 親ビューの .symbolEffect(.pulse) は、すべての子ビュー内のシンボルをパルスさせる
  • .symbolEffectsRemoved() は特定のビューが効果を継承するのを防ぐ
  • リストやツールバーでアニメーションを一括制御するのに適している

アニメーションなしで効果を適用する

場合によっては、シンボルを初期状態から特定の効果状態にしておき、遷移アニメーションは行いたくないことがあります。SwiftUI では Transaction でアニメーションを無効化します(14:55):

Image(systemName: "iphone.radiowaves.left.and.right")
    .symbolEffect(.scale.up, isActive: isScaledUp)
    .onAppear {
        var transaction = Transaction()
        transaction.disablesAnimations = true
        withTransaction(transaction) {
            isScaledUp = true
        }
    }

UIKit/AppKit では animated: false パラメータを直接使います:

imageView.addSymbolEffect(.disappear, animated: false)

Variable Value のなめらかな遷移

iOS 16 で導入された Variable Value は、iOS 17 で自動アニメーションに対応しました(15:44):

struct ContentView: View {
    @State var signalLevel: Double = 0.5
    
    var body: some View {
        Image(systemName: "wifi", variableValue: signalLevel)
    }
}

SwiftUI では variableValue が変化すると、自動でクロスフェードアニメーションが発生します。UIKit では setSymbolImage(_:contentTransition: .automatic) で同じ効果を実現します。

UIKit コントロールの組み込みアニメーション

iOS 17 では一部の UIKit コントロールにシンボルアニメーションが組み込まれています(13:47):

  • UISlider のつまみは両端に到達するとバウンスする
  • UIBarButtonItemaddSymbolEffect をサポートする
  • UIControlUIBarButtonItem には、アニメーション再生を制御する isSymbolAnimationEnabled プロパティが追加された

重要ポイント

ネットワーク状態インジケータに Variable Color アニメーションを追加する

  • 何をするか:ナビゲーションバーやツールバーに Wi-Fi またはルーターのアイコンを置き、接続中に Variable Color アニメーションを再生する
  • 取り組む価値:Discrete + Indefinite 効果の典型的な用途で、接続中は継続再生し、接続成功後になめらかに停止できる
  • 始め方.symbolEffect(.variableColor.iterative.reversing, isActive: isConnecting) を使う

Replace 効果で再生コントロールボタンを改善する

  • 何をするか:再生/一時停止ボタンのアイコン切り替えに Replace アニメーションを追加する
  • 取り組む価値:Replace 効果により 2 つのシンボル間の切り替えに方向感が生まれ、画像を直接差し替えるより質感が高まる
  • 始め方.contentTransition(.symbolEffect(.replace.offUp)) を使う

リスト操作に Bounce フィードバックを追加する

  • 何をするか:ユーザーがお気に入りや削除などのボタンをタップしたとき、アイコンを一度バウンスさせる
  • 取り組む価値:Bounce は Discrete 効果で、操作フィードバックに自然に使え、静的なハイライトより生き生きと見える
  • 始め方.symbolEffect(.bounce, value: tapCount) を使い、タップごとに tapCount を増やす

Disappear 遷移で条件付きビューを作る

  • 何をするか:条件付きで表示するアイコンに .transition(.symbolEffect(.disappear)) を追加し、表示/非表示のアニメーションを付ける
  • 取り組む価値:Transition 動作はレイアウトを変更するため、バッジやヒントマークの表示/非表示に適している
  • 始め方if 条件内の Image に .transition(.symbolEffect(.automatic)) を追加する

関連セッション

コメント

GitHub Issues · utterances