Search code examples
iosswiftswift3uikitcgrect

Place the filled battery inside the image accurately so it scales and aligns for different screens


enter image description here

I want to achieve the above in Swift3 where 60% is the current battery level of the user. The green color must be filled as the battery level increases. I already have a image of the 2 borders i.e. the outermost border, and border within that as 1 image. I want to place the filled color within that. I am having trouble and have to hardcode values as its not getting aligned properly.

The image which is the outermost border and innerborder is called battery which I have placed in xcode as 3x, 2x, 1x.

let view1 = LevelView(frame: CGRect(x: battery.frame.origin.x,
                                        y: battery,
                                        width: battery.layer.preferredFrameSize().width - 25,
                                        height: battery.layer.preferredFrameSize().height / 2 - 5),
                          level: CGFloat(BatteryUtil.sharedInstance.batteryLevel()))

    battery.layer.addSublayer(view1.layer)

Here is LevelView code:

import UIKit

class LevelView : UIView {

  init(frame: CGRect, level: CGFloat) {
    super.init(frame: frame)
    //self.layer.borderWidth = 1.0
    let levelLayer = CAShapeLayer()
    levelLayer.path = UIBezierPath(roundedRect: CGRect(x: frame.origin.x,
                                                       y: frame.origin.y,
                                                       width: frame.width * level,
                                                       height: frame.height),
                                   cornerRadius: 0).cgPath
    levelLayer.fillColor = UIColor(red: 148/255.0, green: 201/255.0, blue: 61/255.0, alpha: 1).cgColor
    self.layer.addSublayer(levelLayer)

  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("Required, but Will not be called in a Playground")
  }
}

I'm quite new to this, how to make it the desired way?


Solution

  • I would suggest getting rid of the bitmap images and just draw the entire shape.

    Try this to start, then adjust settings as needed:

    import UIKit
    
    @IBDesignable
    class LevelView: UIView {
    
        @IBInspectable var batteryLevel: CGFloat = 0.6 {
            didSet {
                setNeedsDisplay()
            }
        }
    
        override func draw(_ rect: CGRect) {
            drawLevel(batteryLevel: batteryLevel)
        }
    
        func drawLevel(batteryLevel: CGFloat = 0.6) {
            //// General Declarations
            let context = UIGraphicsGetCurrentContext()!
    
    
            //// Variable Declarations
            let width: CGFloat = 334 * batteryLevel
            let batteryLabelText = "\(Int(round(batteryLevel * 100)))" + "%"
    
            //// White Rectangle Drawing
            let whiteRectanglePath = UIBezierPath(rect: CGRect(x: 24.5, y: 20.5, width: 334, height: 118))
            UIColor.white.setFill()
            whiteRectanglePath.fill()
            UIColor.black.setStroke()
            whiteRectanglePath.lineWidth = 5
            whiteRectanglePath.stroke()
    
    
            //// Green Rectangle Drawing
            let greenRectangleRect = CGRect(x: 24.5, y: 20.5, width: width, height: 118)
            let greenRectanglePath = UIBezierPath(rect: greenRectangleRect)
            UIColor.green.setFill()
            greenRectanglePath.fill()
            UIColor.black.setStroke()
            greenRectanglePath.lineWidth = 5
            greenRectanglePath.stroke()
            let greenRectangleStyle = NSMutableParagraphStyle()
            greenRectangleStyle.alignment = .center
            let greenRectangleFontAttributes = [
                NSFontAttributeName: UIFont(name: "HelveticaNeue-Bold", size: 12)!,
                NSForegroundColorAttributeName: UIColor.red,
                NSParagraphStyleAttributeName: greenRectangleStyle,
                ]
    
            let greenRectangleTextHeight: CGFloat = batteryLabelText.boundingRect(with: CGSize(width: greenRectangleRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: greenRectangleFontAttributes, context: nil).height
            context.saveGState()
            context.clip(to: greenRectangleRect)
            batteryLabelText.draw(in: CGRect(x: greenRectangleRect.minX, y: greenRectangleRect.minY + (greenRectangleRect.height - greenRectangleTextHeight) / 2, width: greenRectangleRect.width, height: greenRectangleTextHeight), withAttributes: greenRectangleFontAttributes)
            context.restoreGState()
    
    
            //// Outer Rectangle Drawing
            let outerRectanglePath = UIBezierPath(roundedRect: CGRect(x: 7, y: 7, width: 372, height: 146), cornerRadius: 20)
            UIColor.black.setStroke()
            outerRectanglePath.lineWidth = 12
            outerRectanglePath.stroke()
    
    
            //// Bezier Drawing
            let bezierPath = UIBezierPath()
            bezierPath.move(to: CGPoint(x: 396, y: 53))
            bezierPath.addLine(to: CGPoint(x: 396, y: 109))
            bezierPath.addLine(to: CGPoint(x: 407, y: 98))
            bezierPath.addLine(to: CGPoint(x: 407, y: 64))
            bezierPath.addLine(to: CGPoint(x: 396, y: 53))
            bezierPath.close()
            UIColor.gray.setFill()
            bezierPath.fill()
            UIColor.black.setStroke()
            bezierPath.lineWidth = 12
            bezierPath.lineCapStyle = .round
            bezierPath.lineJoinStyle = .round
            bezierPath.stroke()
        }
    
    
    }
    

    You can add it directly to your storyboard if you want.

    LevelView item in storyboard

    Add a reference from your storyboard to your view controller.

    Add a reference from your storyboard to your view controller