0

Based on the answer to this question: Animate drawing of a circle

I now want to display two of these circles simultaneously on the same screen but in two different views. If I just want to animate one circle there are no problems. However if I try to add a second, only the second one is visible.

This is the Circle class:

import UIKit

var circleLayer: CAShapeLayer!

class Circle: UIView {

init(frame: CGRect, viewLayer: CALayer) {

    super.init(frame: frame)
    self.backgroundColor = UIColor.clear

    // Use UIBezierPath as an easy way to create the CGPath for the layer.
    // The path should be the entire circle.
    let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)

    // Setup the CAShapeLayer with the path, colors, and line width
    circleLayer = CAShapeLayer()
    circleLayer.path = circlePath.cgPath
    circleLayer.fillColor = UIColor.clear.cgColor
    circleLayer.strokeColor = UIColor.green.cgColor
    circleLayer.lineWidth = 8.0;

    // Don't draw the circle initially
    circleLayer.strokeEnd = 0.0

    // Add the circleLayer to the view's layer's sublayers
    viewLayer.addSublayer(circleLayer)
}

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

func animateCircle(duration: TimeInterval) {

    // We want to animate the strokeEnd property of the circleLayer
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    // Set the animation duration appropriately
    animation.duration = duration

    // Animate from 0 (no circle) to 1 (full circle)
    animation.fromValue = 0
    animation.toValue = 1

    // Do a linear animation (i.e The speed of the animation stays the same)
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

    // Set the circleLayer's strokeEnd property to 1.0 now so that it's the
    // Right value when the animation ends
    circleLayer.strokeEnd = 1.0

    // Do the actual animation
    circleLayer.add(animation, forKey: "animateCircle")
}

func removeCircle() {
    circleLayer.strokeEnd = 0.0
}

}

And here is how I call it from my ViewController:

var rythmTimer: Circle?
var adrenalineTimer: Circle?

override func viewDidLoad() {

// Create two timers as circles
    self.rythmTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view1.layer)
    if let rt = rythmTimer {
        view1.addSubview(rt)
        rt.center = CGPoint(x: self.view1.bounds.midX, y: self.view1.bounds.midY);
    }

    self.adrenalineTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view2.layer)

    if let at = adrenalineTimer {
        view2.addSubview(at)
        at.center = CGPoint(x: self.view2.bounds.midX, y: self.view2.bounds.midY)
    }
}

If I remove the code for the adrenalineTimer I can see the circle drawn by the rythmTimer. If I keep it the rythmTimer will be displayed in view2 instead of view1 and will have the duration/color of the rythmTimer

2 Answers2

0

It seems you set the circle path with wrong origin points. In addition, you do it before you place the circle on the view.

Add this code to the animate function instead:

func animateCircle(duration: TimeInterval) {

    let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.origin.x + self.frame.size.width / 2, y: self.frame.origin.y + self.frame.size.height / 2), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)

    circleLayer.path = circlePath.cgPath

    // We want to animate the strokeEnd property of the circleLayer
    let animation = CABasicAnimation(keyPath: "strokeEnd")

    ...
Asaf Shveki
  • 736
  • 8
  • 11
0

The main problem is that you have declared circleLayer outside of the class:

import UIKit

var circleLayer: CAShapeLayer!

class Circle: UIView {

    init(frame: CGRect, viewLayer: CALayer) {

        super.init(frame: frame)
        self.backgroundColor = UIColor.clear

        ...
    }

}

The result is that you only ever have ONE circleLayer object (instance).

If you move it inside the class, then each instance of Circle will have its own instance of circleLayer:

import UIKit

class Circle: UIView {

    var circleLayer: CAShapeLayer!

    init(frame: CGRect, viewLayer: CALayer) {

        super.init(frame: frame)
        self.backgroundColor = UIColor.clear

        ...
    }

}

There are a number of other odd things you're doing, but that is why you only get one animated circle.


Edit: Here's a modified version of your code that will allow you to use auto-layout instead of fixed / hard-coded sizes and positions. You can run this directly in a Playground page:

import UIKit
import PlaygroundSupport

class Circle: UIView {

    var circleLayer: CAShapeLayer!

    override init(frame: CGRect) {

        super.init(frame: frame)
        self.backgroundColor = UIColor.clear

        // Setup the CAShapeLayer with colors and line width
        circleLayer = CAShapeLayer()
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.strokeColor = UIColor.green.cgColor
        circleLayer.lineWidth = 8.0;

        // We haven't set the path yet, so don't draw initially
        circleLayer.strokeEnd = 0.0

        // Add the layer to the self's layer
        self.layer.addSublayer(circleLayer)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()

        // Use UIBezierPath as an easy way to create the CGPath for the layer.
        // The path should be the entire circle.
        // this will update whenever the frame size changes (makes it easy to use with auto-layout)
        let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
        circleLayer.path = circlePath.cgPath

    }

    func animateCircle(duration: TimeInterval) {

        // We want to animate the strokeEnd property of the circleLayer
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        // Set the animation duration appropriately
        animation.duration = duration

        // Animate from 0 (no circle) to 1 (full circle)
        animation.fromValue = 0
        animation.toValue = 1

        // Do a linear animation (i.e The speed of the animation stays the same)
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

        // Set the circleLayer's strokeEnd property to 1.0 now so that it's the
        // Right value when the animation ends
        circleLayer.strokeEnd = 1.0

        // Do the actual animation
        circleLayer.add(animation, forKey: "animateCircle")
    }

    func removeCircle() {
        circleLayer.strokeEnd = 0.0
    }

}

class MyViewController : UIViewController {

    var rythmTimer: Circle?
    var adrenalineTimer: Circle?

    var theButton: UIButton = {
        let b = UIButton()
        b.setTitle("Tap Me", for: .normal)
        b.translatesAutoresizingMaskIntoConstraints = false
        b.backgroundColor = .red
        return b
    }()

    var view1: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .blue
        return v
    }()

    var view2: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .orange
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white

        view.addSubview(theButton)
        // constrain button to Top: 32 and centerX
        NSLayoutConstraint.activate([
            theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
            theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
            ])

        // add an action for the button tap
        theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)

        view.addSubview(view1)
        view.addSubview(view2)

        NSLayoutConstraint.activate([
            view1.widthAnchor.constraint(equalToConstant: 120.0),
            view1.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
            view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            view1.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
            view2.widthAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
            view2.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
            view2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 40.0),
            ])

        rythmTimer = Circle(frame: CGRect.zero)
        adrenalineTimer = Circle(frame: CGRect.zero)

        if let rt = rythmTimer,
            let at = adrenalineTimer {
            view1.addSubview(rt)
            view2.addSubview(at)
            rt.translatesAutoresizingMaskIntoConstraints = false
            at.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                rt.widthAnchor.constraint(equalToConstant: 100.0),
                rt.heightAnchor.constraint(equalToConstant: 100.0),
                rt.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
                rt.centerYAnchor.constraint(equalTo: view1.centerYAnchor),
                at.widthAnchor.constraint(equalToConstant: 100.0),
                at.heightAnchor.constraint(equalToConstant: 100.0),
                at.centerXAnchor.constraint(equalTo: view2.centerXAnchor),
                at.centerYAnchor.constraint(equalTo: view2.centerYAnchor),
                ])
        }

    }

    // on button tap, change the text in the label(s)
    @objc func didTap(_ sender: Any?) -> Void {
        if let rt = rythmTimer,
            let at = adrenalineTimer {
            rt.removeCircle()
            at.removeCircle()
            rt.animateCircle(duration: 2.0)
            at.animateCircle(duration: 2.0)
        }
    }

}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
DonMag
  • 69,424
  • 5
  • 50
  • 86