SwiftUIがSF Symbols Artを進化させる。try! Swift Tokyo 2024でのトークの裏側

2024.03.23

日本最大級のSwiftのカンファレンス try! Swift Tokyo 2024で登壇してきました。

try! Swiftは、世界中からSwift愛好者が集結し、日頃のSwiftの知識やスキルを披露し共有するカンファレンスです。

オープニングを始まる前に撮影したのですが、人が沢山いて緊張感を隠しきれませんでした。(Apple Watchからもアラートが表示される)

786名の方が今回のカンファレンスに参加されると知り、鼓動が更に速くなります、、

登壇内容

SF Symbolsの芸術的世界:限りない可能性を解き放つというテーマで登壇させていただきました。かれこれ3年近くSF Symbolsの魅力に囚われ、研究しているSF Symbols Artについての発表でした。

今回は英語での登壇でしたが、日本人の方も海外の方も楽しんでもらえたようなのでとても良かったです。

昨年11月のアルゼンチン登壇での反省点を生かすべく、色々と工夫と練習をしたのでそれ成果を出せた?ようなので一安心です。

発表資料

アート作品

アップロードした資料がPDFでアニメーションが表示されない為、いくつか作品をピックアップして載せます。

Nosebleed

Arm hair

Pants

Fireworks

その他の作品は、SFSymbolsArtCollectionをビルドすると確認できます。

認定証

今回は講習を受けてくれた方(発表を聞いてくれた方)に認定証を渡したいと思い、Apple Walletに登録されるPassを用意しました。

SwiftUIがSF Symbolsを進化させる

実は今回の登壇で伝えたかった裏テーマはSwiftUIがSF Symbolsを進化されるというポイントでした。しかし、今回は5分~10分という枠で伝える必要があり、資料を削り取っていく中でコードに関する部分は削り取らざるを得ない状態になってしまいました。

この場を借りて、SF SymbolsがSwiftUIと共にどう進化してきたかお伝えしていきます。

iOS 15

SymbolRenderingModeが追加されました。

SymbolRenderingMode

この機能を使うことでよりSF Symbolsを華やかにでき、表現をより魅力的なものにすることができます。.symbolRenderingModeモディファイアを使用することで簡単に使用できます。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 32) {
            VStack {
                Image(systemName: "cloud.heavyrain.fill")
                    .font(.system(size: 60))
                    .symbolRenderingMode(.monochrome)
                    .foregroundStyle(.gray)

                Text("monochrome")
            }

            VStack {
                Image(systemName: "cloud.heavyrain.fill")
                    .font(.system(size: 60))
                    .symbolRenderingMode(.hierarchical)
                    .foregroundStyle(.gray)

                Text("hierarchical")
            }

            VStack {
                Image(systemName: "cloud.heavyrain.fill")
                    .font(.system(size: 60))
                    .symbolRenderingMode(.palette)
                    .foregroundStyle(.gray, .blue)

                Text("palette")
            }

            VStack {
                Image(systemName: "cloud.heavyrain.fill")
                    .font(.system(size: 60))
                    .symbolRenderingMode(.multicolor)
                    .foregroundStyle(.gray)

                Text("multicolor")
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.cyan.opacity(0.3))
    }
}

それぞれのSymbolRenderingModeの見え方はこのようになります。

SymbolRenderingMode 概要
monochrome 単色のレイヤーとしてレンダリング
hierarchical 設定した色から異なる不透明度で複数のレイヤーとしてレンダリング
palette 設定した複数の色から複数のレイヤーとしてレンダリング
multicolor 独自の色を持っている場合は、その色が適用されてレンダリング

iOS 16

iOS 16からinit(systemName:variableValue:)を使用できるようになり、可変カラーをシンボルに適用できるようになりました。

しかし、全てのシンボルに可変カラーを使用できるわけではありません。

可変カラー適用のシンボルについては、SF Symbolsのアプリの可変カラーを選択すると確認できます。

variableValueの値を変化させることで、シンボルの見た目を変化させることができます。

struct ContentView: View {

    @State private var value: Double = 0
    var body: some View {

        VStack {
            Image(systemName: "rainbow", variableValue: value)
                .symbolRenderingMode(.multicolor)
                .font(.system(size: 100))

            Slider(value: $value)
                .padding()
        }
    }
}

iOS 17

SymbolEffectが追加され、シンボルにプレセンテーションエフェクトを適用できるようになりました。

SymbolEffect

SymbolEffectで使用できるエフェクトは8種類あります。

SymbolEffect 概要
appear シンボルの画像のレイヤーを個別にまたはまとめて表示するアニメーション
automatic シンボルの画像にデフォルトのアニメーションをコンテキストに応じて適用するトランジション
bounce シンボルの画像のレイヤーに一時的なスケーリング効果、またはバウンスを適用するアニメーション
disappear シンボルの画像のレイヤーを個別にまたはまとめて消去するアニメーション
pulse シンボルの画像のいくつかまたはすべてのレイヤーの不透明度をフェードするアニメーション
replace 1つのシンボルベースの画像のレイヤーを別のものと置き換えるアニメーション
scale シンボルの画像のレイヤーを個別にまたはまとめてスケーリングするアニメーション
variableColor シンボルベースの画像の可変レイヤーの不透明度を繰り返しシーケンスで置き換えるアニメーション

今回はbounceの使用例を紹介します。

bounce
struct ContentView: View {

    @State private var counter: Int = 0

    var body: some View {

        Image(systemName: "heart.fill")
            .symbolRenderingMode(.multicolor)
            .font(.system(size: 100))
            .symbolEffect(.bounce, value: counter)
            .onAppear {
                Timer.scheduledTimer(withTimeInterval: 0.6, repeats: true) { timer in
                    counter += 1

                    if counter > 10 {
                        timer.invalidate()
                    }
                }
            }
    }
}

今回はTimerを使用してcounterをカウントアップさせ、counterの値が切り替わる度にbounceのエフェクトが発生するようにしました。

KeyframeAnimator

また、iOS 17からKeyframeAnimatorが使用できるようになり、これによって気軽に細かいアニメーションを行うことができるようになりました。これはSF SymbolsのImageのみに適用されるものではなく、Viewに対して使用することができます。

まずはアニメーションを行うためのAnimationValueを定義します。

struct AnimationValue {
    var fontSize: CGFloat
    var positionX: CGFloat
    var positionY: CGFloat
}

そして、keyframeAnimatorモディファイアを使用し、初期値と各KeyFrame毎にどの時間でどのような値に変化させたいかを設定します。

struct ContentView: View {

    @State private var isAnimating = false

    var body: some View {

        ZStack(alignment: .bottom) {

            Image(systemName: "eyeglasses")
                .keyframeAnimator(initialValue: AnimationValue(fontSize: 30,
                                                               positionX: 40,
                                                               positionY: 100),
                                  trigger: isAnimating) { content, value in
                    content
                        .font(.system(size: value.fontSize))
                        .position(x: value.positionX,
                                  y: value.positionY)
                } keyframes: { _ in
                    KeyframeTrack(\.fontSize) {
                        CubicKeyframe(100, duration: 2)
                    }

                    KeyframeTrack(\.positionX) {
                        CubicKeyframe(200, duration: 2)
                    }

                    KeyframeTrack(\.positionY) {
                        CubicKeyframe(100, duration: 2)
                    }
                }

            Image(systemName: "tirepressure")
                .keyframeAnimator(initialValue: AnimationValue(fontSize: 30,
                                                               positionX: 100,
                                                               positionY: 100),
                                  trigger: isAnimating) { content, value in
                    content
                        .font(.system(size: value.fontSize))
                        .position(x: value.positionX,
                                  y: value.positionY)
                } keyframes: { _ in
                    KeyframeTrack(\.fontSize) {
                        CubicKeyframe(100, duration: 2)
                    }

                    KeyframeTrack(\.positionX) {
                        CubicKeyframe(200, duration: 2)
                    }

                    KeyframeTrack(\.positionY) {
                        CubicKeyframe(200, duration: 2)
                    }
                }

            Image(systemName: "tirepressure")
                .keyframeAnimator(initialValue: AnimationValue(fontSize: 30,
                                                               positionX: 100,
                                                               positionY: 100),
                                  trigger: isAnimating) { content, value in
                    content
                        .font(.system(size: value.fontSize))
                        .position(x: value.positionX,
                                  y: value.positionY)
                } keyframes: { _ in
                    KeyframeTrack(\.fontSize) {
                        CubicKeyframe(100, duration: 2)
                    }

                    KeyframeTrack(\.positionX) {
                        CubicKeyframe(200, duration: 2)
                    }

                    KeyframeTrack(\.positionY) {
                        CubicKeyframe(200, duration: 2)
                    }
                }

            Image(systemName: "light.panel")
                .keyframeAnimator(initialValue: AnimationValue(fontSize: 30,
                                                               positionX: 160,
                                                               positionY: 100),
                                  trigger: isAnimating) { content, value in
                    content
                        .font(.system(size: value.fontSize))
                        .position(x: value.positionX,
                                  y: value.positionY)
                } keyframes: { _ in
                    KeyframeTrack(\.fontSize) {
                        CubicKeyframe(100, duration: 2)
                    }

                    KeyframeTrack(\.positionX) {
                        CubicKeyframe(200, duration: 2)
                    }

                    KeyframeTrack(\.positionY) {
                        CubicKeyframe(320, duration: 2)
                    }
                }

            Button("Start animating") {
                isAnimating = true
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

上記の例だとこのように動作します。

アニメーションを簡単に追加できるようになり、SF Symbols Artの魅了が更に高まりました。

おわりに

駆け足になってしまいましたが、SF Symbolsの進化について説明させていただきました。

こう見てみると、SF Symbolsで使用できる機能が一年に一回は追加されているのでこれからも目を離せませんね。

引き続き、SF Symbolsの進化を追っていきたいと思います!

try! Swiftはまだまだ続きます。残りも楽しんでいきます!

参考