How to transform a circle into a line in swift? - swift

So i have a circe with a certain pattern. I want to make transform this circle into a line. Any ideas how i could accomplish this?
I did have a look at making a UIBezierPath over the circle for then to transform this into a line without any help. I've also seen a link doing this in opencv (fast Cartesian to Polar to Cartesian in Python) but do prefer to do this in native swift.

If you don't care about the nature of the transition, the easiest way is to do CABasicAnimation, animating the path of a CAShapeLayer from one path to another. And to achieve a patterned path with CAShapeLayer is actually to have two overlapping CAShapeLayer objects, one with a dash pattern on top of one without.
let fromPath = UIBezierPath(arcCenter: view.center, radius: 100, startAngle: 0, endAngle: CGFloat(M_PI) * 2.0, clockwise: true)
let toPath = UIBezierPath()
toPath.moveToPoint(CGPoint(x: view.frame.size.width / 2.0 - CGFloat(M_PI) * 100.0, y:view.center.y))
toPath.addLineToPoint(CGPoint(x: view.frame.size.width / 2.0 + CGFloat(M_PI) * 100.0, y: view.center.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = fromPath.CGPath
shapeLayer.lineWidth = 5
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.fillColor = UIColor.clearColor().CGColor
let shapeLayer2 = CAShapeLayer()
shapeLayer2.path = fromPath.CGPath
shapeLayer2.lineWidth = 5
shapeLayer2.strokeColor = UIColor.blackColor().CGColor
shapeLayer2.fillColor = UIColor.clearColor().CGColor
shapeLayer2.lineDashPattern = [100,50]
view.layer.addSublayer(shapeLayer)
view.layer.addSublayer(shapeLayer2)
And
shapeLayer.path = toPath.CGPath
shapeLayer2.path = toPath.CGPath
CATransaction.begin()
CATransaction.setAnimationDuration(5)
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = fromPath.CGPath
animation.toValue = toPath.CGPath
shapeLayer.addAnimation(animation, forKey: nil)
let animation2 = CABasicAnimation(keyPath: "path")
animation2.fromValue = fromPath.CGPath
animation2.toValue = toPath.CGPath
shapeLayer2.addAnimation(animation, forKey: nil)
CATransaction.commit()
Yielding:
If you want more control over the nature of the transition, then you have to resort to more manual techniques, e.g. a CADisplayLink in which you manually adjust the path. But that's more complicated.

try this code, The animation is not perfect, but you will be able to convert an image to a line this way. I have to say that Rob's solution above is far more superior and if I were give a choice, i would do it his way.
The below code is very self-explanatory, but if in case of any doubt, feel free to ask.
To make the animation happen, tap the yellow button.
class ViewController: UIViewController{
var imageView : UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.layer.cornerRadius = 120
image.backgroundColor = UIColor.clearColor()
image.layer.borderWidth = 3.0
image.layer.borderColor = UIColor.blackColor().CGColor
return image
}()
var animatorButton : UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = UIColor.blueColor()
button.layer.borderWidth = 3.0
button.layer.borderColor = UIColor.blackColor().CGColor
button.backgroundColor = UIColor.yellowColor()
return button
}()
var constraintHeight = [NSLayoutConstraint]()
override func viewDidLoad() {
view.addSubview(imageView)
view.addSubview(animatorButton)
view.addConstraint(view.centerXAnchor.constraintEqualToAnchor(imageView.centerXAnchor))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[imageView(240)]", options: NSLayoutFormatOptions(), metrics: nil, views: ["imageView": imageView]))
self.constraintHeight = NSLayoutConstraint.constraintsWithVisualFormat("V:|-100-[imageView(240)]", options: NSLayoutFormatOptions(), metrics: nil, views: ["imageView": imageView])
view.addConstraints(constraintHeight)
view.addConstraint(animatorButton.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-450-[Button(40)]", options: NSLayoutFormatOptions(), metrics: nil, views: ["Button": animatorButton]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[Button]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["Button": animatorButton]))
animatorButton.addTarget(self, action: #selector(ViewController.animateImageToLine), forControlEvents: .TouchUpInside)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func animateImageToLine() {
view.removeConstraints(constraintHeight)
constraintHeight = NSLayoutConstraint.constraintsWithVisualFormat("V:|-100-[imageView(3)]", options: NSLayoutFormatOptions(), metrics: nil, views: ["imageView": imageView])
view.addConstraints(constraintHeight)
UIView.animateWithDuration(0.3, animations: {
self.imageView.layer.cornerRadius = 0
self.view.layoutIfNeeded()
}, completion: { success in
self.imageView.backgroundColor = UIColor.blackColor()
self.imageView.layer.borderWidth = 3.0
})
}
}

Related

How do I set an endpoint for a CAKeyFrameAnimation?

I am using an arc for a progress bar.
There is a track layer that the progress indicator moves along and I also have a circle that moves with the progress bar for added emphasis. Both the progress bar and the circle move correctly along the path but when the progress is complete, instead of stopping at the end of the path, the circle jumps back to the beginning of the progress path.
screen recording of behavior
The code:
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let dotLayer: CALayer = CALayer()
let seconds = 10.0
// MARK: - Properties
private var progressBarView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
title = "Making Progress"
view.backgroundColor = .systemTeal
// determine the value of Center
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: 2.7, endAngle: 0.45, clockwise: true) // these values give us a complete circle
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
view.layer.addSublayer(shapeLayer)
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap )))
}
#objc private func handleTap() {
print("Attempting to animate stroke")
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = seconds
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "anyKeyWillDo")
// circle image
let circleNobImage = UIImage(named: "whiteCircle.png")!
dotLayer.contents = circleNobImage.cgImage
dotLayer.bounds = CGRect(x: 0.0, y: 0.0, width: circleNobImage.size.width, height: circleNobImage.size.height)
view.layer.addSublayer(dotLayer)
dotLayer.position = CGPoint(x: 105, y: 460)
dotLayer.opacity = 1
dotAnimation()
}
func dotAnimation() {
let dotAnimation = CAKeyframeAnimation(keyPath: "position")
dotAnimation.path = trackLayer.path
dotAnimation.calculationMode = .paced
let dotAnimationGroup = CAAnimationGroup()
dotAnimationGroup.duration = seconds
dotAnimation.autoreverses = false
dotAnimationGroup.animations = [dotAnimation]
dotLayer.add(dotAnimationGroup, forKey: nil)
}
}
I need to get the circle to stop at the end of the arc with the progress bar. How is this done?
Also set dotAnimationGroup.fillMode and dotAnimationGroup.isRemovedOnCompletion
dotAnimationGroup.fillMode = .forwards
dotAnimationGroup.isRemovedOnCompletion = false

Circle Progress Bar for Swift Count down

Inside the circle progress bar there is a constantly changing number (eg 250, 300, 1000). Whenever I click, the number will decrease and circle progress bar will move. I did it with the time counter. But I want to do it with my control. So when I click the button it will move, if I don't click it won't move.
My code :
import UIKit
class ViewController: UIViewController {
let shapeLAyer = CAShapeLayer()
let progressLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 2*CGFloat.pi, clockwise: true)
progressLayer.path = circularPath.cgPath
// ui edits
progressLayer.strokeColor = UIColor.black.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
//progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 20.0
view.layer.addSublayer(progressLayer)
shapeLAyer.path = circularPath.cgPath
shapeLAyer.fillColor = UIColor.clear.cgColor
shapeLAyer.strokeColor = UIColor.red.cgColor
shapeLAyer.lineWidth = 10
shapeLAyer.lineCap = .round
shapeLAyer.strokeEnd = 0
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handle)))
view.layer.addSublayer(shapeLAyer)
}
#objc func handle(){
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 3
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
}
[![enter image description here][1]][1]
You can achieve this type of progress by setting proper from value, to value and with the mode of CABasicAnimation
Here, I take two var one for total number (for total progress) and one for current progress number.
From these two var, I converted this value to between 0-1.
First, in your code UIBezierPath angle is wrong.
Replace this line by
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 3*CGFloat.pi / 2, clockwise: true)
class ViewController: UIViewController {
// Other code
private let totalNumber: CGFloat = 300.0
private var currentProgressNumber: CGFloat = 0
private let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
// viewDidLoad code
func playAnimation(){
if currentProgressNumber > totalNumber || currentProgressNumber < 0 {
return
}
basicAnimation.fromValue = basicAnimation.toValue
basicAnimation.toValue = currentProgressNumber/totalNumber
basicAnimation.isRemovedOnCompletion = false
basicAnimation.duration = 3
basicAnimation.fillMode = CAMediaTimingFillMode.both
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
#IBAction func onIncrement(_ sender: UIButton) {
currentProgressNumber += 15 //<-- Change your increment interval here
playAnimation()
}
#IBAction func onDecrement(_ sender: UIButton) {
currentProgressNumber -= 15 //<-- Change your increment interval here
playAnimation()
}
}
Note: Replace your animation function with playAnimation() and also change the increment-decrement value as per your requirement for testing I used 15.

Setting the frame of a transformed CALayer

I'm currently playing with CALayers a bit. For demo purposes I'm creating a custom UIView that renders a fuel gauge. The view has two sub-layers:
one for the background
one for the hand
The layer that represents the hand is then simple rotated accordingly to point at the correct value. So far, so good. Now I want the view to resize its layers whenever the size of the view is changed. To achieve this, I created an override of the layoutSubviews method like this:
public override func layoutSubviews()
{
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds)
{
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
As the method is being called many times, I'm using previousBounds to make sure I only perform the update on the layers when the size has actually changed.
At first, I had just the following code in the updateLayers method to set the frames of the sub-layers:
backgroundLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
That worked fine - until the handLayer was rotated. In that case some weird things happen to its size. I suppose it is because the frame gets applied after the rotation and of course, the rotated layer doesn't actually fit the bounds and is thus resized to fit.
My current solution is to temporarily create a new CATransaction that suppresses animations, reverting the transformation back to identity, setting the frame and then re-applying the transformation like this:
CATransaction.begin()
CATransaction.setDisableActions(true)
let oldTransform = scaleLayer.transform
handLayer.transform = CATransform3DIdentity
handLayer.frame = bounds.insetBy(dx: 5, dy: 5)
handLayer.transform = oldTransform
CATransaction.commit()
I already tried omitting the CATransaction and instead applying the handLayer.affineTransform to the bounds I'm setting, but that didn't yield the expected results - maybe I did it wrong (side question: How to rotate a given CGRect around its center without doing all the maths myself)?
My question is simply: Is there a recommended was of setting the frame of a transformed layer or is the solution I found already "the" way to do it?
EDIT
Kevvv provided some sample code, which I've modified to demonstrate my problem:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
customView.backgroundColor = UIColor.blue
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
handLayer.backgroundColor = UIColor.red.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
handLayer.transform = CATransform3DMakeRotation(5, 0, 0, 1)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UITapGestureRecognizer) {
customView.frame = customView.frame.insetBy(dx:10, dy:10)
/*let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)*/
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
If you add this to a playground, then run and tap the control, you'll see what I mean. Watch the red "square".
Do you mind explaining what the "weird things happening to the size" means? I tried to replicate it, but couldn't find the unexpected effects:
class ViewController: UIViewController {
let customView = CustomView(frame: .init(origin: .init(x: 200, y: 200), size: .init(width: 200, height: 200)))
let backgroundLayer = CALayer()
let handLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(customView)
backgroundLayer.backgroundColor = UIColor.yellow.cgColor
backgroundLayer.frame = customView.bounds
let handPath = UIBezierPath()
handPath.move(to: backgroundLayer.position)
handPath.addLine(to: .init(x: 0, y: backgroundLayer.position.y))
handLayer.frame = customView.bounds
handLayer.path = handPath.cgPath
handLayer.strokeColor = UIColor.black.cgColor
customView.layer.addSublayer(backgroundLayer)
customView.layer.addSublayer(handLayer)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
customView.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UITapGestureRecognizer) {
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
let fromValue = self.handLayer.transform
let toValue = CGFloat.pi * 2
animation.duration = 2
animation.fromValue = fromValue
animation.toValue = toValue
animation.valueFunction = CAValueFunction(name: .rotateZ)
self.handLayer.add(animation, forKey: nil)
}
}
class CustomView: UIView {
var previousBounds: CGRect!
override func layoutSubviews() {
super.layoutSubviews()
if previousBounds == nil || !previousBounds!.equalTo(self.bounds) {
previousBounds = self.bounds
self.updateLayers(self.bounds)
}
}
func updateLayers(_ bounds: CGRect) {
guard let sublayers = self.layer.sublayers else { return }
for sublayer in sublayers {
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
}
}
}
Edit
I think the issue is that the red box is resized with a frame. Since a frame is always upright even if it's rotated, if you were to do an inset from a frame, it'd look like this:
However, if you were to resize the red box with bounds:
sublayer.bounds = bounds.insetBy(dx: 5, dy: 5)
sublayer.position = self.convert(self.center, from: self.superview)
instead of:
sublayer.frame = bounds.insetBy(dx: 5, dy: 5)
You'll probably have to re-center the handPath and everything else in it accordingly as well.

How to dispose from UIView subviews on one ViewController when navigating away from it

I'm starting to learn Swift and decided to build an app but without storyboard.
My SceneDelegate scene function instantiates a TabBarController
window = UIWindow(frame: UIScreen.main.bounds)
let tb = TabBarController()
window?.rootViewController = tb
window?.makeKeyAndVisible()
window?.windowScene = windowSceme
I have a TabBarController that extends from UITabBarController which pretty much styles the tab bar and sets it all up
For each item of the Tabbar I have a ViewController. For the purpose of this question I'm going to specify the first "Home".
In Home, which extends ViewController, I have the following
let homePageView = HomePageView()
override func viewWillLayoutSubviews() {
navigationController?.isNavigationBarHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
homePageView.setupHomePage()
view.addSubview(homePageView)
}
override func viewWillDisappear(_ animated: Bool) {
homePageView.dispose()
}
The Controller is pretty much only in charge of calling whatever is going to be displayed on the screen within HomePageView(), the main component. This last, holds two more views. One carrousel HomePageCarrousel() and one header HomePageSocialUp(). This view instantiates the two latter referred to and sets the layout programmatically and adds them as subviews. It also includes a dispose function that sets the instantiated classes to nil such as --> Instantiate - homePageCarrousel = HomePageCarrousel() and the dispose function has homePageCarrousel = nil
That works perfectly fine but when I navigate away from the current view controller via the tab bar and navigate back to it, now I have two instances of HomePageCarrousel() and HomePageSocialUp() within HomeView
I'm probably holding a strong reference somewhere but I can't figure out when. Could someone point me to where should I look to debug it or what might be that is creating the issue.
I'm also providing the code for the two views duplicated in case the issue is there
HomePageSocialUp
class HomePageSocialUp: UIView {
let logo = UIImage(named: "LogoSmiles")
let socialImages: [UIImage] = [UIImage(named: "tripadvisor")!, UIImage(named: "instagram")!, UIImage(named: "facebook")!, UIImage(named: "linkedin")!]
func setupHeaderCircle() {
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
let circlePath = UIBezierPath(arcCenter: CGPoint(x: UIScreen.main.bounds.width / 2, y: insetTop + 60), radius: CGFloat(90), startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
layer.addSublayer(shapeLayer)
}
func setupSocialHeader() {
setupHeaderCircle()
layer.masksToBounds = true;
backgroundColor = UIColor.TintColor
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOpacity = 0.8
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = 3.0
layer.masksToBounds = false
frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 280)
let imageView = UIImageView()
guard let logo = logo else {return}
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
imageView.frame = CGRect(x: CGFloat(Int((UIScreen.main.bounds.width - (logo.size.width)))) / 2, y: 120 - (logo.size.width / 2), width: logo.size.width, height: logo.size.height)
imageView.image = logo
addSubview(imageView)
}
}
HomePageCarrousel
class HomePageCarrousel: UIScrollView {
var images: [UIImage]?
var originX = 0
var numberOfIterations = 0
var timer: Timer?
func setupCarrousel() {
let x = (Int(UIScreen.main.bounds.width) - (Int(UIScreen.main.bounds.width) - 60)) / 2
frame = CGRect(x: x, y: (Int(frame.origin.y) - 350) / 2, width: Int(UIScreen.main.bounds.width) - 60, height: 350)
let newImage = textToImage(drawText: "Creating Smiles in unique places.", frame: frame, inImage: UIImage(named: "smiles1")!, atPoint: CGPoint(x: frame.origin.x + 20, y: frame.height - 20))
images = [newImage, UIImage(named: "smiles2")!]
guard timer == nil else { return }
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(startScrolling), userInfo: nil, repeats: true)
guard let imageCount = images?.count, let images = images else { return }
contentSize = CGSize(width: frame.width * CGFloat(images.count), height: frame.height)
for image in 0..<imageCount {
let imageView = UIImageView()
imageView.image = images[image]
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let xPosition = frame.width * CGFloat(image)
imageView.frame = CGRect(x: xPosition, y: 0, width: frame.width, height: frame.height)
addSubview(imageView)
}
timer?.fire()
}
func textToImage(drawText text: String, frame: CGRect, inImage image: UIImage, atPoint point: CGPoint) -> UIImage {
let textColor = UIColor.white
let textFont = UIFont(name: "Helvetica Bold" , size: 12)!
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(image.size, false, scale)
let textFontAttributes = [
NSAttributedString.Key.font: textFont,
NSAttributedString.Key.foregroundColor: textColor,
] as [NSAttributedString.Key : Any]
image.draw(in: CGRect(origin: CGPoint.zero, size: image.size))
text.draw(in: frame, withAttributes: textFontAttributes)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
#objc func startScrolling() {
print(originX)
guard let images = images else { return }
if originX == images.count {
originX = 0
numberOfIterations += 1
}
if numberOfIterations > 2 {
timer?.invalidate()
timer = nil
numberOfIterations = 0
}
let x = CGFloat(originX) * frame.size.width
setContentOffset(CGPoint(x: x, y: 0), animated: true)
originX += 1
}
}
Thanks upfront
Did you tried to remove the subviews, something like
homePageView.subviews.forEach { (view) in
//taking appropriate action to whichever view you want
view.removeFromSuperview()//for removing views
}

Issue with CABasicAnimation

So basically I am trying to create dialogView. This dialogView will contain a loading spinner and some text which will be updated depending on the state. I followed two guides to use a CAShapeLayer to draw a circle and then animate a line around it.
Link to code/guides I followed
https://www.youtube.com/watch?v=O3ltwjDJaMk
https://github.com/vinayjn/Spinner/blob/master/Spinner.swift
Despite following most of these tutorials to the tee. My animation will not work. At the moment it just looks like this and will not spin.
class LoginDialogView: UIViewController {
lazy var loaderView: UIView = {
let loaderView = UIView()
loaderView.backgroundColor = .yellow
return loaderView
}()
lazy var dialogTitle : UILabel = {
let dialogTitle = UILabel()
dialogTitle.text = "Apples"
dialogTitle.font = UIFont.systemFont(ofSize: 24.0)
dialogTitle.textAlignment = .left
dialogTitle.numberOfLines = 1
return dialogTitle
}()
private var shouldAddSublayer: Bool {
/*
check if:
1. we have any sublayers at all, if we don't then its safe to add a new, so return true
2. if there are sublayers, see if "our" layer is there, if it is not, return true
*/
guard let sublayers = loaderView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "progress"}).count == 0
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
fileprivate func setupView() {
view.backgroundColor = .red
view.layer.cornerRadius = 20
view.addSubview(loaderView)
loaderView.snp.makeConstraints { (make) in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(10)
make.centerX.equalTo(view.safeAreaLayoutGuide.snp.centerX)
make.height.width.equalTo(100)
}
}
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: UIColor.init(red: 56/255, green: 25/255, blue: 49/255, alpha: 1), fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
loaderView.layer.addSublayer(trackLayer)
startAnimating()
}
private func startAnimating(){
animateRing()
}
private func animateRing(){
let rotationAnimation = CABasicAnimation(keyPath: "strokeEnd")
rotationAnimation.fromValue = 0 * (CGFloat.pi / 180)
rotationAnimation.toValue = 360 * (CGFloat.pi / 180)
rotationAnimation.duration = 1.6
rotationAnimation.repeatCount = HUGE
loaderView.layer.add(rotationAnimation, forKey: "strokeEnd")
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint.zero
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 30, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.strokeColor = strokeColor.cgColor
layer.strokeStart = 0
layer.strokeEnd = 0.5
layer.lineCap = .round
layer.lineWidth = 5
layer.position = CGPoint(x: loaderView.frame.size.width / 2, y: loaderView.frame.size.height / 2)
layer.name = "progress"
return layer
}
}
I have looked over and compared the code a couple times and I can't see what I did that was different. So my question is can anyone look over this code and figure out what I did wrong? and Can anyone recommend a safe way to stop this animation that wouldn't cause any potential memory leaks?
Im using ios 13 and xcode 11.3.1
You are not adding the animation to the shape layer; you added it to the container view's layer. Keep a reference to your trackLayer and add the animation to that layer.
class LoginDialogView: UIViewController {
lazy var loaderView: UIView = {
let loaderView = UIView()
loaderView.backgroundColor = .yellow
return loaderView
}()
var trackLayer: CAShapeLayer?
...
...
...
private func setupCircleLayers() {
trackLayer = createCircleShapeLayer(strokeColor: UIColor(red: 56/255,
green: 25/255,
blue: 49/255,
alpha: 1),
fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
loaderView.layer.addSublayer(trackLayer!)
startAnimating()
}
...
...
...
private func animateRing(){
let rotationAnimation = CABasicAnimation(keyPath: "strokeEnd")
rotationAnimation.fromValue = 0 * (CGFloat.pi / 180)
rotationAnimation.toValue = 360 * (CGFloat.pi / 180)
rotationAnimation.duration = 1.6
rotationAnimation.repeatCount = HUGE
trackLayer.add(rotationAnimation, forKey: "strokeEnd")
}
...
...
...
}
Note: ... in the above code refers to the truncated portion of your code.