Benjamin Aronsson
Benjamin Aronsson

Reputation: 51

How do I get the coordinates from CAShapeLayer

So I am trying to make a progress bar. So I have made circular path, but I want the dot to be at the end of the progress bar, but how do I get the position of the dot to be att the end of the current progress?

private func simpleShape() {
  let width: CGFloat = 10
  createCircle()

  //make circle transparant in middle
  progressLayer.fillColor = UIColor.clear.cgColor
  progressLayer.strokeColor = UIColor.blue.cgColor
  progressLayer.lineCap = CAShapeLayerLineCap.round
  progressLayer.lineWidth = width
  progressLayer.strokeStart = 0
  progressLayer.strokeEnd = 0

  //unfilled
  backLayer.lineWidth = width
  backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
  backLayer.strokeEnd = 1

  self.layer.addSublayer(gradientLayer)
}

private func createCircle() {

  //create circle
  let circle = UIView(frame: bounds)
  circle.layoutIfNeeded()
  let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
  let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83

  let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)

  //add layers
  progressLayer.path = circlePath.cgPath
  backLayer.path = circlePath.cgPath
  circle.layer.addSublayer(backLayer)
  circle.layer.addSublayer(progressLayer)
  addSubview(circle)

  circle.layer.addSublayer(dotLayer)
}

let dotLayer = CAShapeLayer()

public func setProgress(_ progress: CGFloat) {
  progressLayer.strokeEnd = CGFloat(progress)

  if let progressEndpoint = progressLayer.path?.currentPoint {
    dotLayer.position = progressEndpoint
  }
}

This is what I'm getting

This is what I'm getting

This is what I want

This is what I want

Upvotes: 1

Views: 950

Answers (2)

Jawad Ali
Jawad Ali

Reputation: 14397

What you need is rotation Animation

let progressLayer = CAShapeLayer()
    let backLayer = CAShapeLayer()
    private func simpleShape() {
      let width: CGFloat = 15
      createCircle()

      //make circle transparant in middle
      progressLayer.fillColor = UIColor.clear.cgColor
      progressLayer.strokeColor = #colorLiteral(red: 0.888897419, green: 0.5411034822, blue: 0.04008810222, alpha: 1)
      progressLayer.lineCap = CAShapeLayerLineCap.round
      progressLayer.lineWidth = width
      progressLayer.strokeStart = 0
        progressLayer.strokeEnd = 0

      //unfilled
      backLayer.lineWidth = width
      backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1)
      backLayer.strokeEnd = 1

     // self.layer.addSublayer(gradientLayer)
    }

    private func createCircle() {

      //create circle
      let circle = UIView(frame: bounds)
      let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
      let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
      let distance = circle.bounds.width / 2 * 0.17


      let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)

      //add layers
      progressLayer.path = circlePath.cgPath
      backLayer.path = circlePath.cgPath
      circle.layer.addSublayer(backLayer)
      circle.layer.addSublayer(progressLayer)
      addSubview(circle)


        let circleCenter = CGPoint(x:centerPoint.x - distance,y:centerPoint.y - circleRadius )

         let dotCircle = UIBezierPath()


        dotCircle.addArc(withCenter:circleCenter, radius: 3, startAngle: CGFloat(-90).deg2rad(), endAngle: CGFloat(270).deg2rad(), clockwise: true)

        dotLayer.path = dotCircle.cgPath
        dotLayer.position = CGPoint(x:centerPoint.x,y:centerPoint.y )
        dotLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.6496753961)
        dotLayer.lineWidth = 10
        dotLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        dotLayer.isHidden = true

      circle.layer.addSublayer(dotLayer)
    }

    let dotLayer = CAShapeLayer()

    public func setProgress(_ progress: CGFloat) {
        print(progress)
//      progressLayer.strokeEnd = progress
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.beginTime = CACurrentMediaTime() + 0.5;
        animation.fromValue = 0
        animation.toValue = progress
        animation.duration = 2
        animation.autoreverses = false
        animation.repeatCount = .nan
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        progressLayer.add(animation, forKey: "line")


        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.dotLayer.isHidden = false
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
       // rotateAnimation.beginTime = CACurrentMediaTime() + 0.5;
        rotateAnimation.fromValue = (CGFloat( -90)).deg2rad()
        rotateAnimation.toValue = (360*progress - 98).deg2rad()
        rotateAnimation.duration = 2
        rotateAnimation.fillMode = .forwards
        rotateAnimation.isRemovedOnCompletion = false
        self.dotLayer.add(rotateAnimation, forKey: nil)
        }

    }

enter image description here

Upvotes: 1

Rob
Rob

Reputation: 437552

You’re going to have to calculate it yourself. So figure out the angle from the start and end angles for your arcs:

let angle = (endAngle - startAngle) * progress + startAngle

And then use basic trigonometry to determine where that point falls:

let point = CGPoint(x: centerPoint.x + radius * cos(angle),
                    y: centerPoint.y + radius * sin(angle))

dotLayer.position = point

By the way, I’d suggest separating the adding of the sublayers (which is part of the initial configuration process) from the updating paths (which is part of the view layout process, which may be called again if the frame of the view changes, constraints are applied, etc). Thus, perhaps:

@IBDesignable
class ProgressView: UIView {
    var progress: CGFloat = 0 { didSet { updateProgress() } }

    private var centerPoint: CGPoint = .zero
    private var radius: CGFloat = 0
    private let startAngle: CGFloat = -0.475 * .pi
    private let endAngle: CGFloat = 1.525 * .pi
    private let lineWidth: CGFloat = 10

    private lazy var progressLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.blue.cgColor
        shapeLayer.lineCap = .round
        shapeLayer.lineWidth = lineWidth
        shapeLayer.strokeStart = 0
        shapeLayer.strokeEnd = progress
        return shapeLayer
    }()

    private lazy var backLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = lineWidth
        shapeLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
        return shapeLayer
    }()

    private lazy var dotLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = UIBezierPath(arcCenter: .zero, radius: lineWidth / 2 * 1.75, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
        shapeLayer.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor
        return shapeLayer
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        addSublayers()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addSublayers()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        updatePaths()
        updateProgress()
    }
}

private extension ProgressView {
    func addSublayers() {
        layer.addSublayer(backLayer)
        layer.addSublayer(progressLayer)
        layer.addSublayer(dotLayer)
    }

    func updatePaths() {
        centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
        radius = min(bounds.width, bounds.height) / 2 * 0.83

        let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

        progressLayer.path = circlePath.cgPath
        backLayer.path = circlePath.cgPath
    }

    func updateProgress() {
        progressLayer.strokeEnd = progress

        let angle = (endAngle - startAngle) * progress + startAngle
        let point = CGPoint(x: centerPoint.x + radius * cos(angle),
                            y: centerPoint.y + radius * sin(angle))

        dotLayer.position = point
    }
}

Upvotes: 5

Related Questions