Search code examples
swiftcore-graphicsuibezierpath

setLineDash symmetrically in Swift 2/3


I'm developing an internet speed test app. Something I will do to practice and learn more about a future project.

This is my Swift Code:

import UIKit

@IBDesignable
class Arc: UIView {

   @IBInspectable var dashWidth: CGFloat = 12.0
   @IBInspectable var smallDashWidth: CGFloat = 5.0

   override func draw(_ rect: CGRect) {
       // Position and Radius of Arc
       let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

       // Calculate Angles
       let π = CGFloat(M_PI)
       let startAngle = 3 * π / 4
       let endAngle = π / 4
       let radius = max(bounds.width / 1.15, bounds.height / 1.15) / 2 - dashWidth / 2

       // Start Context
       let context = UIGraphicsGetCurrentContext()


       // MARK: Base
       let arc = UIBezierPath(arcCenter: center, radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: true)
       arc.addArc(withCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
       arc.lineJoinStyle = .bevel
       arc.lineCapStyle = .round
       arc.close()
       UIColor.yellow().setStroke()
       arc.lineWidth = smallDashWidth
       //context!.saveGState()
       context!.setLineDash(phase: 0, lengths: [0, 0], count: 2)
       arc.stroke()
       context!.saveGState()


       // MARK: dash Arc
       let dashArc = UIBezierPath()
       dashArc.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

       // Round Line
       dashArc.lineJoinStyle = .round;

       // Set Stroke and Width of Dash
       UIColor.white().setStroke()
       dashArc.lineWidth = dashWidth

       // Save Context and Set Line Dash
       context!.saveGState()
       context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)

       // Draw Line
       dashArc.stroke()

       // Restore Context
       context!.restoreGState()
   }

}

The result is this:

enter image description here


What I need to do:

I need to automate this line of code:

context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)

The lengths are [2, 54] which numbers are added without calculation, only to get the final equation number taken to obtain this dynamically.     1: need to add dashes 12 (which may later be changed, being assigned as a variable) across the arc. The arc begins and ends at a variable angle (possibility to change later).     2: The value of dashArc.lineWidth = dashWidth can also be changed, and is an important item to calculate the space between the 12 dashes.     3: Since all the variables presented values can be variable, which is the best way to do this calculation.     4: The first and the last dash should be at the same angle that the respective startAngle and endAngle.


What I need:

I need a calculation that looks and spreads as symmetrically as possible the dashes during the arc.

I thought of a similar calculation to this:

var numberOfDashes = 12
var perimeterArc = ?
var widthDash = 2

spacing = (perimeterArc - (widthDash * numberOfDashes)) / numberOfDashes

context!.setLineDash(phase: 0, lengths: [widthDash, spacing], count: 2)

But I do not know how to calculate the perimeterArc. Can someone help me? I could not think of anything to create a logical calculation for this in Swift 2/3.

I appreciate any tip.


Solution

  • Instead of trying to compute spaces directly by trying to use a dash pattern, first of all, think in terms of angles.

    I lifted some code that was originally created as an example in my book PostScript By Example (page 281). I transliterated the postscript code to Swift (version 2, as version 3 can't seem to do anything useful).

    I also eschewed your use of UIBezierPath mixed in with an access to the Graphics Context, since I have the feeling that there are some strange interactions between the two. UIBezierPath is intended to manage the Graphics Context under the covers.

    Here is the code:

    class  ComputeDashes : UIView
    

    {

    let insetAmount: CGFloat = 40.0

    let  numberOfDashes: Int     = 16
    let  startAngle:     CGFloat = CGFloat(1.0 * M_PI / 4.0)
    let  endAngle:       CGFloat = CGFloat(3.0 * M_PI / 4.0)
    let  subtendedAngle: CGFloat = CGFloat(2.0 * M_PI) - CGFloat(2.0 * M_PI / 4.0)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init(coder: NSCoder) {
        super.init(coder: coder)!
    }
    
    override func drawRect(rect: CGRect)
    {
        let  insets: UIEdgeInsets = UIEdgeInsetsMake(insetAmount, insetAmount, insetAmount, insetAmount)
        let  newbounds: CGRect = UIEdgeInsetsInsetRect(self.bounds, insets)
    
        let  centre: CGPoint = CGPointMake(CGRectGetMidX(newbounds), CGRectGetMidY(newbounds))
        let  radius: CGFloat = CGRectGetWidth(newbounds) / 2.0
    
        let  context: CGContext = UIGraphicsGetCurrentContext()!
    
        CGContextAddArc(context, centre.x, centre.y, radius, startAngle, endAngle, 1)
    
        CGContextSetLineWidth(context, 10.0)
        UIColor.magentaColor().set()
        CGContextStrokePath(context)
    
        //    MARK:  paint dashes
    
        let  innerRadius: CGFloat = radius * 0.75
        CGContextSaveGState(context)
        CGContextTranslateCTM(context, centre.x, centre.y)
    
        let  angle = subtendedAngle / CGFloat(numberOfDashes)
        CGContextRotateCTM(context, endAngle)
        for rot in 0...numberOfDashes {
            let  innerPoint: CGPoint = CGPointMake(innerRadius, 0.0)
            CGContextMoveToPoint(context, innerPoint.x, innerPoint.y)
            let  outerPoint: CGPoint = CGPointMake(radius, 0.0)
            CGContextAddLineToPoint(context, outerPoint.x, outerPoint.y)
            CGContextRotateCTM(context, angle)
        }
        CGContextSetLineWidth(context, 2.0)
        UIColor.blackColor().set()
        CGContextStrokePath(context)
        CGContextRestoreGState(context)
    }
    

    }

    I believe that this approach is much more flexible, and avoids the tricky computations you would need to do to account for line widths in the 'on' phase of the dash pattern.

    I hope this helps. Let me know what you think.