🔋

SwiftUIでバッテリーアニメーションを作成する方法

2023/07/21に公開

はじめに

この記事では、SwiftUIを使用してバッテリーの充電レベルを表示するアニメーションを作成する方法を説明していきます。

完成したバッテリーアニメーションは以下のようになります。

環境

  • Xcode: 14.3.1
  • iOS: 16.4
  • Swift: 5.8.1

実装

バッテリーの充電状態を表示

まずはバッテリーを表示するソースコードをBatteryAnimationView.swiftにまとめていきます。
充電量(progress)を与えられた時に、残りの充電量をバッテリーの見た目とテキストで表現出来るところまで実装していきましょう。

title=BatteryAnimationView.swift
import SwiftUI

struct BatteryAnimationView: View {
    @Binding var progress: Double
    let size: CGFloat

    var body: some View {
        ZStack {
            HStack(spacing: 0) {
                // Battery frame
                Rectangle()
                    .stroke(.black, lineWidth: size / 50)
                    .frame(width: size, height: size / 2)
                    .background(
                        // Battery gauge
                        Rectangle()
                            .fill(.green)
                            .scaleEffect(
                                x: progress,
                                y: 1,
                                anchor: .leading
                            )
                    )
                // Battery head
                Rectangle()
                    .fill(.black)
                    .frame(width: size / 20, height: size / 5)
            }
            
            Text("\(Int(self.progress * 100))%")
                .foregroundColor(.black)
                .font(.system(size: size / 6))
        }
    }
}

struct BatteryAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        BatteryAnimationView(progress: .constant(0.7), size: 300)
    }
}

ContentView.swiftでBatteryAnimationView.swiftを使用します。
充電量の値をランダムに代入するボタンを追加して、テスト出来るようにしましょう。

title=ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var progress = 1.0
    
    var body: some View {
        VStack {
            BatteryAnimationView(progress: $progress, size: 300)
                        
            Button(action: randomizeProgress) {
                Text("Update")
                    .font(.headline)
                    .foregroundColor(.white)
            }
            .padding()
            .background(Color.blue)
            .cornerRadius(10)
        }
    }
    
    private func randomizeProgress() {
        self.progress = Double.random(in: 0.0...1.0)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

挙動は以下のようになります。

色を動的に変更

充電量が残り10%のときは充電ゲージを赤色、残り20%のときは黄色、それ以外の場合は緑色になるようにしましょう。
ゲージの色を表す変数、gaugeColorを導入します。

title=BatteryAnimationView.swift
import SwiftUI

struct BatteryAnimationView: View {
    @Binding var progress: Double
    let size: CGFloat
    
+   private var gaugeColor: Color {
+       if progress <= 0.1 {
+           return .red
+       } else if progress <= 0.2 {
+           return .yellow
+       } else {
+           return .green
+       }
+   }

    var body: some View {
        ZStack {
            HStack(spacing: 0) {
                // Battery frame
                Rectangle()
                    .stroke(.black, lineWidth: size / 50)
                    .frame(width: size, height: size / 2)
                    .background(
                        // Battery gauge
                        Rectangle()
-                           .fill(.green)
+                           .fill(gaugeColor)
                            .scaleEffect(
                                x: progress,
                                y: 1,
                                anchor: .leading
                            )
                    )
                // Battery head
                Rectangle()
                    .fill(.black)
                    .frame(width: size / 20, height: size / 5)
            }
            
            Text("\(Int(self.progress * 100))%")
                .foregroundColor(.black)
                .font(.system(size: size / 6))
        }
    }
}

struct BatteryAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        BatteryAnimationView(progress: .constant(0.7), size: 300)
    }
}

挙動は以下のようになります。

アニメーションの追加

最後にアニメーションを追加します。
withAnimationを使用して、onAppearには画面の表示時のアニメーションの処理、randomizeProgressに充電量が変化したときのアニメーションの処理を追加します。

title=ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var progress = 0.0
    
    var body: some View {
        VStack {
            BatteryAnimationView(progress: $progress, size: 300)
                        
            Button(action: randomizeProgress) {
                Text("Update")
                    .font(.headline)
                    .foregroundColor(.white)
            }
            .padding()
            .background(Color.blue)
            .cornerRadius(10)
        }
+       .onAppear {
+           withAnimation(.interpolatingSpring(
+               stiffness: 20.0,
+               damping: 8.0)) {
+               self.progress = 1.0
+           }
+       }
    }
    
    private func randomizeProgress() {
-       self.progress = Double.random(in: 0.0...1.0)
+       withAnimation(.interpolatingSpring(stiffness: 20.0, damping: 8.0)) {
+           self.progress = Double.random(in: 0.0...1.0)
+       }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

テキストのアニメーションは必要ないので、テキストのみをアニメーション処理の対象外とします。

title=BatteryAnimationView.swift
import SwiftUI

struct BatteryAnimationView: View {
    @Binding var progress: Double
    let size: CGFloat
    
    private var gaugeColor: Color {
        if progress <= 0.1 {
            return .red
        } else if progress <= 0.2 {
            return .yellow
        } else {
            return .green
        }
    }

    var body: some View {
        ZStack {
            HStack(spacing: 0) {
                // Battery frame
                Rectangle()
                    .stroke(.black, lineWidth: size / 50)
                    .frame(width: size, height: size / 2)
                    .background(
                        // Battery gauge
                        Rectangle()
                            .fill(gaugeColor)
                            .scaleEffect(
                                x: progress,
                                y: 1,
                                anchor: .leading
                            )
                    )
                // Battery head
                Rectangle()
                    .fill(.black)
                    .frame(width: size / 20, height: size / 5)
            }
            
            Text("\(Int(self.progress * 100))%")
                .foregroundColor(.black)
                .font(.system(size: size / 6))
+               .animation(nil)
        }
    }
}

struct BatteryAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        BatteryAnimationView(progress: .constant(0.7), size: 300)
    }
}

挙動は以下のようになります。(最初に示した動画と同じ)

さいごに

GitHubからソースコードを得る場合はこちら

https://github.com/Ru2Lu/swiftui-battery-animation

参考URL

Flutterにおけるバッテリーアニメーションの実装
https://islamdidarmd.medium.com/animated-battery-charge-with-custom-painter-in-flutter-a7dec8bfddf8

SwiftUIにおけるバッテリーアニメーションの実装
https://github.com/federicoazzu/AnimatableBattery/tree/main

Discussion