Swift: Animate object along animating path - swift

I would like to animate little red dot rotating around circle that is expanding in a pulse manner (go from small to big, then start back from small). It seems that little dot keeps rotating around original shape and does not take into account that circle it's expanding... I have this in code:
// MARK: - Properties
private lazy var containerView = UIView()
let littleCircleRadius: CGFloat = 10
private lazy var littleRedDot: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.red.cgColor
let littleDotSize = CGSize(width: 10, height: 10)
layer.frame = CGRect(x: containerView.bounds.center.x - littleDotSize.width / 2,
y: containerView.bounds.center.y - littleCircleRadius - littleDotSize.width/2 ,
width: littleDotSize.width,
height: littleDotSize.height)
return layer
}()
private lazy var littleCircleLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = 1.5
layer.lineCap = .round
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
return layer
}()
// MARK: - Setup
func setup() {
view.addSubview(containerView)
containerView.frame = CGRect(x: 40, y: 200, width: 300, height: 300)
containerView.backgroundColor = UIColor.gray.withAlphaComponent(0.2)
littleCircleLayer.path = makeArcPath(arcCenter: containerView.bounds.center, radius: 10)
containerView.layer.addSublayer(littleCircleLayer)
containerView.layer.addSublayer(littleRedDot)
}
// MARK: - Animations
func animate() {
CATransaction.begin()
CATransaction.setAnimationDuration(1.5)
animateLittleRedDotRotation()
animateCircleExpanding()
CATransaction.commit()
}
func animateLittleRedDotRotation() {
let anim = CAKeyframeAnimation(keyPath: "position")
anim.duration = 1.5
anim.rotationMode = .rotateAuto
anim.repeatCount = Float.infinity
anim.path = littleCircleLayer.path
littleRedDot.add(anim, forKey: "rotate")
}
func animateCircleExpanding() {
let maxCircle = makeArcPath(arcCenter: containerView.bounds.center, radius: 100)
let circleExpandingAnim = CABasicAnimation(keyPath: "path")
circleExpandingAnim.fromValue = littleCircleLayer.path
circleExpandingAnim.toValue = maxCircle
circleExpandingAnim.repeatCount = Float.infinity
circleExpandingAnim.duration = 1.5
littleCircleLayer.add(circleExpandingAnim, forKey: "pulseCircuitAnimation")
}
This creates following effect:
However I would like to achieve for little dot to be rotating along the expanding circle path (as it animates from small circle to bigger circle), not the original small circle path. Any ideas ?

Using CoreAnimation to animate the position of the red dot based upon the path assumes that the path isn't changing. You could, theoretically, define a spiral path that mirrors the expanding circle. Personally, I'd just use CADisplayLink, a special timer designed optimally for screen refreshes, and retire the CoreAnimation calls entirely. E.g.
func startDisplayLink() {
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
let radius = ...
let center = containerView.bounds.center
circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
let angle = percent * .pi * 2
let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
}
That yields:
The full example:
class ViewController: UIViewController {
private let radiusRange: ClosedRange<CGFloat> = 10...100
private let duration: CGFloat = 1.5
private lazy var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
private lazy var redDot: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.red.cgColor
return layer
}()
private lazy var circleLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = 1.5
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
return layer
}()
private weak var displayLink: CADisplayLink?
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startDisplayLink()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopDisplayLink()
}
}
// MARK: Private utility methods
private extension ViewController {
func setup() {
addContainer()
containerView.layer.addSublayer(circleLayer)
containerView.layer.addSublayer(redDot)
}
func addContainer() {
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
func makeArcPath(arcCenter: CGPoint, radius: CGFloat) -> CGPath {
UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}
// MARK: - DisplayLink related methods
private extension ViewController {
func startDisplayLink() {
stopDisplayLink() // stop existing display link, if any
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
func stopDisplayLink() {
displayLink?.invalidate()
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
let radius = radiusRange.percent(percent)
let center = containerView.bounds.center
circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
let angle = percent * .pi * 2
let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
}
}
// MARK: - CGRect extension
extension CGRect {
var center: CGPoint { return CGPoint(x: midX, y: midY) }
}
// MARK: - ClosedRange extension
extension ClosedRange where Bound: FloatingPoint {
func percent(_ percent: Bound) -> Bound {
(upperBound - lowerBound) * percent + lowerBound
}
}

Related

Using SwiftUI or UIKit to position view elements?

This is more or less a repost of a question that I think could have been reproduced more minimally. I'm trying to form a text bubble using the triangle created by the overriden draw() function and the rest of the callout. I removed all the code that couldn't possibly affect the positioning of either the triangle or the box.
Recap: I'd like to move the triangle created by the draw function outside of the frame created in the initialization (Currently, it's inside the frame).
If I can add anything to clarify or make this question better, let me know.
class CustomCalloutView: UIView, MGLCalloutView {
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
//MARK: Subviews -
required init() {
// init with 75% of width and 120px tall
super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width:
UIScreen.main.bounds.width * 0.75, height: 120)))
setup()
}
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
func setup() {
// setup this view's properties
self.backgroundColor = UIColor.white
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect,
animated: Bool) {
//Always, Slightly above center
self.center = view.center.applying(CGAffineTransform(translationX: 0, y: -self.frame.height))
view.addSubview(self)
}
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .black
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y +
rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}

How to generate a custom UIView that is above a UIImageView that has a circular hole punched in the middle that can see through to the UIImageView?

I am trying to punch a circular hole through a UIView that is above a UIImageView, whereby the hole can see through to the image below (I would like to interact with this image through the hole with a GestureRecognizer later). I have 2 problems, I cannot get the circular hole to centre to the middle of the UIImageView (it is currently centred to the top left of the screen), and that the effect that I am getting is the opposite to what i am trying to achieve (Everything outside of the circle is visible). Below is my code. Please can someone advise?
Result:
class UploadProfileImageViewController: UIViewController {
var scrollView: ReadyToUseScrollView!
let container: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let imgView: UIImageView = {
let imgView = UIImageView()
imgView.backgroundColor = UIColor.black
imgView.contentMode = .scaleAspectFit
imgView.image = UIImage.init(named: "soldier")!
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
var overlay: UIView!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup(){
view.backgroundColor = UIColor.white
setupViews()
}
override func viewDidLayoutSubviews() {
overlay.center = imgView.center
print("imgView.center: \(imgView.center)")
overlay.layer.layoutIfNeeded() // I have also tried view.layoutIfNeeded()
}
private func setupViews(){
let s = view.safeAreaLayoutGuide
view.addSubview(imgView)
imgView.topAnchor.constraint(equalTo: s.topAnchor).isActive = true
imgView.leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
imgView.trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
imgView.heightAnchor.constraint(equalTo: s.heightAnchor, multiplier: 0.7).isActive = true
overlay = Overlay.init(frame: .zero, center: imgView.center)
print("setup.imgView.center: \(imgView.center)")
view.addSubview(overlay)
overlay.translatesAutoresizingMaskIntoConstraints = false
overlay.topAnchor.constraint(equalTo: s.topAnchor).isActive = true
overlay.leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
overlay.trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
overlay.bottomAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
}
private func deg2rad( number: Double) -> CGFloat{
let rad = number * .pi / 180
return CGFloat.init(rad)
}
}
class Overlay: UIView{
var path: UIBezierPath!
var viewCenter: CGPoint?
init(frame: CGRect, center: CGPoint) {
super.init(frame: frame)
self.viewCenter = center
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(){
backgroundColor = UIColor.black.withAlphaComponent(0.8)
guard let path = createCirclePath() else {return}
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.layer.mask = shapeLayer
}
private func createCirclePath() -> UIBezierPath?{
guard let center = self.viewCenter else{return nil}
let circlePath = UIBezierPath()
circlePath.addArc(withCenter: center, radius: 200, startAngle: 0, endAngle: deg2rad(number: 360), clockwise: true)
return circlePath
}
private func deg2rad( number: Double) -> CGFloat{
let rad = number * .pi / 180
return CGFloat.init(rad)
}
}
CONSOLE:
setup.imgView.center: (0.0, 0.0)
imgView.center: (207.0, 359.0)
Try getting rid of the overlay you have and instead add the below UIView. It's basically a circular UIView with a giant black border, but it takes up the whole screen so the user can't tell. FYI, you need to use .frame to position items on the screen. The below puts the circle in the center of the screen. If you want the center of the image, replace self.view.frame with self. imgView.frame... Play around with circleSize and borderSize until you get the circle size you want.
let circle = UIView()
let circleSize: CGFloat = self.view.frame.height * 2 //must be bigger than the screen
let x = (self.view.frame.width / 2) - (circleSize / 2)
let y = (self.view.frame.height / 2) - (circleSize / 2)
circle.frame = CGRect(x: x, y: y, width: circleSize, height: circleSize)
let borderSize = (circleSize / 2) * 0.9 //the size of the inner circle will be circleSize - borderSize
circle.backgroundColor = .clear
circle.layer.cornerRadius = circle.frame.height / 2
circle.layer.borderColor = UIColor.black.cgColor
circle.layer.borderWidth = borderSize
view.addSubview(circle)

Change color gradient around donut view

I am trying to make an animated donut view that when given a value between 0 and 100 it will animate round the view up to that number. I have this working fine but want to fade the color from one to another, then another on the way around. Currently, when I add my gradient it goes from left to right and not around the circumference of the donut view.
class CircleScoreView: UIView {
private let outerCircleLayer = CAShapeLayer()
private let outerCircleGradientLayer = CAGradientLayer()
private let outerCircleLineWidth: CGFloat = 5
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
buildLayers()
}
/// Value must be within 0...100 range
func setScore(_ value: Int, animated: Bool = false) {
if value != 0 {
let clampedValue: CGFloat = CGFloat(value.clamped(to: 0...100)) / 100
if !animated {
outerCircleLayer.strokeEnd = clampedValue
} else {
let outerCircleAnimation = CABasicAnimation(keyPath: "strokeEnd")
outerCircleAnimation.duration = 1.0
outerCircleAnimation.fromValue = 0
outerCircleAnimation.toValue = clampedValue
outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
outerCircleLayer.strokeEnd = clampedValue
outerCircleLayer.add(outerCircleAnimation, forKey: "outerCircleAnimation")
}
outerCircleGradientLayer.colors = [Constant.Palette.CircleScoreView.startValue.cgColor,
Constant.Palette.CircleScoreView.middleValue.cgColor,
Constant.Palette.CircleScoreView.endValue.cgColor]
}
}
private func buildLayers() {
// Outer background circle
let arcCenter = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let startAngle = CGFloat(-0.5 * Double.pi)
let endAngle = CGFloat(1.5 * Double.pi)
let circlePath = UIBezierPath(arcCenter: arcCenter,
radius: (frame.size.width - outerCircleLineWidth) / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// Outer circle
setupOuterCircle(outerCirclePath: circlePath)
}
private func setupOuterCircle(outerCirclePath: UIBezierPath) {
outerCircleLayer.path = outerCirclePath.cgPath
outerCircleLayer.fillColor = UIColor.clear.cgColor
outerCircleLayer.strokeColor = UIColor.black.cgColor
outerCircleLayer.lineWidth = outerCircleLineWidth
outerCircleLayer.lineCap = CAShapeLayerLineCap.round
outerCircleGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
outerCircleGradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
outerCircleGradientLayer.frame = bounds
outerCircleGradientLayer.mask = outerCircleLayer
layer.addSublayer(outerCircleGradientLayer)
}
}
I am going for something like this but the color isn't one block but gradients around the donut view from one color to the next.
If you imported AngleGradientLayer into your project then all you should need to do is change:
private let outerCircleGradientLayer = CAGradientLayer() to
private let outerCircleGradientLayer = AngleGradientLayer()

Why doesn't UIView.animateWithDuration affect this custom view?

I designed a custom header view that masks an image and draws a border on the bottom edge, which is an arc. It looks like this:
Here's the code for the class:
class HeaderView: UIView
{
private let imageView = UIImageView()
private let dimmerView = UIView()
private let arcShape = CAShapeLayer()
private let maskShape = CAShapeLayer() // Masks the image and the dimmer
private let titleLabel = UILabel()
#IBInspectable var image: UIImage? { didSet { self.imageView.image = self.image } }
#IBInspectable var title: String? { didSet {self.titleLabel.text = self.title} }
#IBInspectable var arcHeight: CGFloat? { didSet {self.setupLayers()} }
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
initMyStuff()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
initMyStuff()
}
override func prepareForInterfaceBuilder()
{
backgroundColor = UIColor.clear()
}
internal func initMyStuff()
{
backgroundColor = UIColor.clear()
titleLabel.font = Font.AvenirNext_Bold(24)
titleLabel.text = "TITLE"
titleLabel.textColor = UIColor.white()
titleLabel.layer.shadowColor = UIColor.black().cgColor
titleLabel.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
titleLabel.layer.shadowRadius = 0.0;
titleLabel.layer.shadowOpacity = 1.0;
titleLabel.layer.masksToBounds = false
titleLabel.layer.shouldRasterize = true
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
dimmerView.frame = self.bounds
dimmerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
addSubview(dimmerView)
addSubview(titleLabel)
// Add the shapes
self.layer.addSublayer(arcShape)
self.layer.addSublayer(maskShape)
self.layer.masksToBounds = true // This seems to be unneeded...test more
// Set constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView .autoPinEdgesToSuperviewEdges()
titleLabel.autoCenterInSuperview()
}
func setupLayers()
{
let aHeight = arcHeight ?? 10
// Create the arc shape
arcShape.path = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight)
arcShape.strokeColor = UIColor.white().cgColor
arcShape.lineWidth = 1.0
arcShape.fillColor = UIColor.clear().cgColor
// Create the mask shape
let maskPath = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight, closed: true)
maskPath.moveTo(nil, x: bounds.size.width, y: bounds.size.height)
maskPath.addLineTo(nil, x: bounds.size.width, y: 0)
maskPath.addLineTo(nil, x: 0, y: 0)
maskPath.addLineTo(nil, x: 0, y: bounds.size.height)
//let current = CGPathGetCurrentPoint(maskPath);
//print(current)
let mask_Dimmer = CAShapeLayer()
mask_Dimmer.path = maskPath.copy()
maskShape.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
maskShape.path = maskPath
// Apply the masks
imageView.layer.mask = maskShape
dimmerView.layer.mask = mask_Dimmer
}
override func layoutSubviews()
{
super.layoutSubviews()
// Let's go old school here...
imageView.frame = self.bounds
dimmerView.frame = self.bounds
setupLayers()
}
}
Something like this will cause it to just snap to the new size without gradually changing its frame:
UIView.animate(withDuration: 1.0)
{
self.headerView.arcHeight = self.new_headerView_arcHeight
self.headerView.frame = self.new_headerView_frame
}
I figure it must have something to do with the fact that I'm using CALayers, but I don't really know enough about what's going on behind the scenes.
EDIT:
Here's the function I use to create the arc path:
class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
// let startAngle = radians(180) + angle;
// let endAngle = radians(360) - angle;
let path = CGMutablePath();
path.addArc(nil, x: arcCenter.x, y: arcCenter.y, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false);
if(closed == true)
{path.addLineTo(nil, x: startPoint.x, y: startPoint.y);}
return path;
}
BONUS:
Setting the arcHeight property to 0 results in no white line being drawn. Why?
The Path property can't be animated. You have to approach the problem differently. You can draw an arc 'instantly', any arc, so that tells us that we need to handle the animation manually. If you expect the entire draw process to take say 3 seconds, then you might want to split the process to 1000 parts, and call the arc drawing function 1000 times every 0.3 miliseconds to draw the arc again from the beginning to the current point.
self.headerView.arcHeight is not a animatable property. It is only UIView own properties are animatable
you can do something like this
let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode
let expectedFramesPerSecond = 60
var diff : CGFloat = 0
func update() {
let diffUpdated = self.headerView.arcHeight - self.new_headerView_arcHeight
let done = (fabs(diffUpdated) < 0.1)
if(!done){
self.headerView.arcHeight -= diffUpdated/(expectedFramesPerSecond*0.5)
self.setNeedsDisplay()
}
}

Swift: How to draw a circle step by step?

I build a project to draw an arc initially, this arc is 1/8 of a circle. Then i put a button on the viewController, whenever I click this button, it will draw another 1/8 of a circle seamless on it .
But I got a problem: when i click the button, it almost draws the arc rapidly(0.25s), not the duration i set before(1s). How to reach that no matter when I click the button, it consumes the same time as the duration i set before?
import UIKit
let π = CGFloat(M_PI)
class ViewController: UIViewController {
var i: CGFloat = 1
var maxStep: CGFloat = 8
var circleLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
let startAngle = CGFloat(0)
let endAngle = 2*π
let ovalRect = CGRectMake(100, 100, 100, 100)
let ovalPath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)), radius: CGRectGetWidth(ovalRect), startAngle: startAngle, endAngle: endAngle, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = ovalPath.CGPath
circleLayer.strokeColor = UIColor.blueColor().CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.lineWidth = 10.0
circleLayer.lineCap = kCALineCapRound
self.circleLayer = circleLayer
self.view.layer.addSublayer(self.circleLayer)
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1.0
anim.fromValue = 0.0
anim.toValue = self.i/self.maxStep
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
self.i++
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// when this action be triggered, it almost draws the arc rapidly, why?
#IBAction func buttonAnimate(sender: UIButton) {
if self.i<=self.maxStep {
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.i++
}
}
}
Get your animation and reset properties. See code below:
#IBAction func buttonAnimate(sender: UIButton) {
if self.i<=self.maxStep {
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.circleLayer?.animationKeys()
let anim = self.circleLayer?.animationForKey("arc animation") as? CABasicAnimation
if anim == nil {
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1
anim.fromValue = (self.i - 1)/self.maxStep
anim.toValue = self.i/self.maxStep
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
}
self.i++
}
}
Hope this helps!
Here is the correct solution:
import UIKit
import UIKit
let π = CGFloat(M_PI)
class ViewController: UIViewController {
var i: CGFloat = 1
var maxStep: CGFloat = 8
var prevValue : CGFloat = 0.0
var circleLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
let startAngle = CGFloat(0)
let endAngle = 2*π
let ovalRect = CGRectMake(100, 100, 100, 100)
let ovalPath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)), radius: CGRectGetWidth(ovalRect), startAngle: startAngle, endAngle: endAngle, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = ovalPath.CGPath
circleLayer.strokeColor = UIColor.blueColor().CGColor
circleLayer.fillColor = nil //UIColor.clearColor().CGColor
circleLayer.lineWidth = 10.0
circleLayer.lineCap = kCALineCapRound
self.circleLayer = circleLayer
self.view.layer.addSublayer(self.circleLayer!)
self.circleLayer!.strokeStart = 0.0
//Initial stroke-
setStrokeEndForLayer(self.circleLayer!, from: 0.0, to: self.i / self.maxStep, animated: true)
self.i++
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonAnimate(sender: UIButton) {
if self.i <= self.maxStep {
setStrokeEndForLayer(self.circleLayer!, from: (self.i - 1 ) / self.maxStep, to: self.i / self.maxStep, animated: true)
self.i++
}
}
func setStrokeEndForLayer(layer: CALayer, var from:CGFloat, to: CGFloat, animated: Bool)
{
self.circleLayer!.strokeEnd = to
if animated
{
//Check if there is any existing animation is in progress, if so override, the from value
if let circlePresentationLayer = self.circleLayer!.presentationLayer()
{
from = circlePresentationLayer.strokeEnd
}
//Remove any on going animation
if (self.circleLayer?.animationForKey("arc animation") as? CABasicAnimation != nil)
{
//Remove the current animation
self.circleLayer!.removeAnimationForKey("arc animation")
}
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1
anim.fromValue = from
anim.toValue = to
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
}
}
}