Search code examples
iosswiftcore-graphicsuibezierpathcashapelayer

How to make universal round draws (with lineDashPattern) with UIbezierPath on Swift


I'm about to make a universal Tuner app but when I try to draw an Analog meter it gets messed on different devices. Maybe it's very hardcoded.. Check this:

enter image description here

The labels are under the same UIView too. Check the code to draw these paths:

override func draw(_ rect: CGRect) {

    let center = CGPoint(x:bounds.width/2, y: bounds.height)

    var radius: CGFloat = max(bounds.width, bounds.height+50)

    // Define the thickness of the arc.
    let arcWidth: CGFloat = 1

    let startAngle: CGFloat = π
    let endAngle: CGFloat = 2 * π

    var path = UIBezierPath(arcCenter: center,
                            radius: radius/2 - arcWidth/2,
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)

    path.lineWidth = arcWidth
    counterColor.setStroke()
    path.stroke()

    radius = max(bounds.width, bounds.height+70)

    path = UIBezierPath(arcCenter: center,
                            radius: radius/2 - arcWidth/2,
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)

    let strokeColor            = UIColor.black.cgColor


    roundThinLayer.path             = path.cgPath
    roundThinLayer.strokeColor      = strokeColor
    roundThinLayer.lineWidth        = 16.0
    roundThinLayer.fillColor        = UIColor.clear.cgColor
    roundThinLayer.lineDashPattern  = [ 0.5, 4.55 ]
    //roundThinLayer.lineDashPhase    = 0.25

    self.layer.addSublayer(roundThinLayer)

    radius = max(bounds.width, bounds.height+90)

    path = UIBezierPath(arcCenter: center,
                        radius: radius/2 - arcWidth/2,
                        startAngle: startAngle,
                        endAngle: endAngle,
                        clockwise: true)

    roundThickLayer.path            = path.cgPath
    roundThickLayer.strokeColor     = strokeColor
    roundThickLayer.lineWidth       = 40
    roundThickLayer.fillColor       = UIColor.clear.cgColor
    roundThickLayer.lineDashPattern = [ 1.5, 140 ]
    roundThickLayer.lineDashPhase   = 0.25
    self.layer.addSublayer(roundThickLayer)
}

Can someone help me get these values without hardcoding? i.e. I want to make the thins/thicks dashes as in the storyboard (that's running on an iPhone 7). I want to make this app universal.

Note: The autoresizing is correctly, right?

Thanks in advance.


Solution

  • @Denis - yeah, I think your hard-coded values are hurting you, particularly for the lineDashPattern.

    Give this a try. It uses the length of the line (arc) and the number of ticks to calculate the "gaps" for the lineDashPattern (will likely need some tweaking for bounds and what-not)).

    public override func draw(_ rect: CGRect) {
    
        let center = CGPoint(x:bounds.width/2, y: bounds.height)
    
        var radius: CGFloat = max(bounds.width, bounds.height+50)
    
        // Define the thickness of the arc.
        let arcWidth: CGFloat = 1
    
        let startAngle: CGFloat = CGFloat(M_PI) // π
        let endAngle: CGFloat = CGFloat(2 * M_PI) // π
    
        let counterColor = UIColor.red
    
        var path = UIBezierPath(arcCenter: center,
                                radius: radius/2 - arcWidth/2,
                                startAngle: startAngle,
                                endAngle: endAngle,
                                clockwise: true)
    
        path.lineWidth = arcWidth
        counterColor.setStroke()
        path.stroke()
    
    
        // init vars for later use
        var nTicks = 0
        var tickWidth = 0.0
        var gapWidth = 0.0
    
    
        radius += 20
    
        path = UIBezierPath(arcCenter: center,
                            radius: radius/2 - arcWidth/2,
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)
    
        let strokeColor            = UIColor.black.cgColor
    
        let roundThinLayer = CAShapeLayer()
    
        // number of short ticks to draw
        nTicks = 150
    
        // thickness of short ticks
        tickWidth = 0.5
    
        // calculate the gap between ticks
        gapWidth = ((M_PI * Double(radius) / 2) - (tickWidth * Double(nTicks))) / Double(nTicks - 1)
    
        roundThinLayer.path             = path.cgPath
        roundThinLayer.strokeColor      = strokeColor
        roundThinLayer.lineWidth        = 20.0
        roundThinLayer.fillColor        = UIColor.clear.cgColor
        roundThinLayer.lineDashPattern  = [ tickWidth as NSNumber, gapWidth as NSNumber ]
        roundThinLayer.lineDashPhase    = CGFloat(tickWidth / Double(2))
    
    
        self.layer.addSublayer(roundThinLayer)
    
    
        radius += 20
    
        path = UIBezierPath(arcCenter: center,
                            radius: radius/2 - arcWidth/2,
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)
    
        let roundThickLayer = CAShapeLayer()
    
    
        // number of tall ticks to draw
        nTicks = 7
    
        // thickness of tall ticks
        tickWidth = 1.5
    
        // calculate the gap between ticks
        gapWidth = ((M_PI * Double(radius) / 2) - (tickWidth * Double(nTicks))) / Double(nTicks - 1)
    
        roundThickLayer.path            = path.cgPath
        roundThickLayer.strokeColor     = strokeColor
        roundThickLayer.lineWidth       = 40
        roundThickLayer.fillColor       = UIColor.clear.cgColor
        roundThickLayer.lineDashPattern = [ tickWidth as NSNumber, gapWidth as NSNumber ]
        roundThickLayer.lineDashPhase   = CGFloat(tickWidth / Double(2))
        self.layer.addSublayer(roundThickLayer)
    
        self.clipsToBounds = true
    
    
    }