iosswiftswiftuibattery

How to create a Battery Badge that shows current Battery Level in SwiftUI


What I want to do is almost identical to the badge on MacOS and iOS devices I tried to use SFSymbols for this but it only supports %0, %25, %50, %75 and %100 but I want to update my battery badge to the current battery level.

img

I've tried to use battery.0 icon in SFSymbols with an RoundedRectangle overlapping it like this:

struct BatteryView : View {
    var body: some View {
            ZStack {
                Image(systemName: "battery.0")
                RoundedRectangle(cornerRadius: 15)
                    .foregroundColor(.green)
            }
    }
}

but it acts really weird when I try to scale it up or down because of the RoundedRectangle.

Thanks in advance!


Solution

  • Here's what I came up with:

    As far as I can see, the battery icon in SFSymbols consists of only 3 elements, a rounded rectangle, a half circle that stands on top of the battery, and another rounded rectangle that indicates the current battery level.

    First rounded Rectangle (Battery Case)

    struct BatteryView : View {
        var body: some View {
            GeometryReader { geo in
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 3)
           } 
       }
    }
    

    and it looks like this(with a frame of 180, 60):

    battery case

    I just used a rounded rectangle for the battery case as you suggested.

    Half circle

    I created a simple shape called HalfCircleShape for this.

    struct HalfCircleShape : Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            
            path.move(to: CGPoint(x: rect.minX, y: rect.midY))
            path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height , startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
            return path
        }
    }
    

    and in the GeometryReader I give a frame of 1/7 of the current frame because it looks elegant.

    and I combine these with a HStack

    battery_2

    like this:

    GeometryReader { geo in
        HStack {
            RoundedRectangle(cornerRadius: 15)
                .stroke(lineWidth: 3)
            
            HalfCircleShape()
                .frame(width: geo.size.width / 7, height: geo.size.height / 7)
        }
    }
    

    Battery Indicator

    I create an extension in Color to choose the color with the current battery level

    
    extension Color {
        static var BatteryLevel : Color {
            let batteryLevel = 0.6
            print(batteryLevel)
            switch batteryLevel {
                // returns red color for range %0 to %20
                case 0...0.2:
                    return Color.red
                // returns yellow color for range %20 to %50
                case 0.2...0.5:
                    return Color.yellow
                // returns green color for range %50 to %100
                case 0.5...1.0:
                    return Color.green
                default:
                    return Color.clear
            }
        }
    }
    

    And I just created another RoundedRectangle and combine the two RoundedRectangles in a ZStack

    Result looks like this: red green yellow

    Final code:

    
    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
                BatteryView()
                .frame(width: 170, height: 60)
                .padding()
        }
    }
    
    struct HalfCircleShape : Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            
            path.move(to: CGPoint(x: rect.minX, y: rect.midY))
            path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height , startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
            return path
        }
    }
    
    struct BatteryView : View {
        init() {
            UIDevice.current.isBatteryMonitoringEnabled = true
        }
        var body: some View {
            // UIDevice.current.batteryLevel always returns -1, and I don't know why. so here's a value for you to preview
            let batteryLevel = 0.4
            GeometryReader { geo in
                HStack(spacing: 5) {
                    GeometryReader { rectangle in
                        RoundedRectangle(cornerRadius: 15)
                            .stroke(lineWidth: 3)
                        RoundedRectangle(cornerRadius: 15)
                            .padding(5)
                            .frame(width: rectangle.size.width - (rectangle.size.width * (1 - batteryLevel)))
                            .foregroundColor(Color.BatteryLevel)
                    }
                    HalfCircleShape()
                    .frame(width: geo.size.width / 7, height: geo.size.height / 7)
                    
                }
                .padding(.leading)
            }
        }
    }
    
    extension Color {
        static var BatteryLevel : Color {
            let batteryLevel = 0.4
            print(batteryLevel)
            switch batteryLevel {
                // returns red color for range %0 to %20
                case 0...0.2:
                    return Color.red
                // returns yellow color for range %20 to %50
                case 0.2...0.5:
                    return Color.yellow
                // returns green color for range %50 to %100
                case 0.5...1.0:
                    return Color.green
                default:
                    return Color.clear
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    Also, in UIDevice, there's a propety called batteryLevel that gives you the current battery level, but in this case, it only returns -1, note: as discussed here, it seems like a bug in Swift.