Sync a stroke fill animation based on a UIProgressView status? - ios

Currently we have a circular progress bar that is working properly, for testing purposes it is activated by a tap gesture.
func createCircleShapeLayer() {
let center = view.center
if UIDevice.current.userInterfaceIdiom == .pad {
let circlePath = UIBezierPath(arcCenter: center, radius: 150, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
circleShapeLayer.path = circlePath.cgPath
} else {
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
circleShapeLayer.path = circlePath.cgPath
}
circleShapeLayer.strokeColor = UIColor.green.cgColor
circleShapeLayer.lineWidth = 20
circleShapeLayer.fillColor = UIColor.clear.cgColor
circleShapeLayer.lineCap = CAShapeLayerLineCap.round
circleShapeLayer.strokeEnd = 0
view.layer.addSublayer(circleShapeLayer)
//tap gesture used for animation testing
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(anitmateCirleProgress)))
}
#objc func anitmateCirleProgress() {
let strokeAnimation = CABasicAnimation(keyPath: strokeEnd)
strokeAnimation.toValue = 1
strokeAnimation.duration = 2
strokeAnimation.fillMode = .forwards
strokeAnimation.isRemovedOnCompletion = false
circleShapeLayer.add(strokeAnimation, forKey: "strokeAnimation")
}
The issue is that the progress bar has to be able to fill based on the status of a UIProgressView: ex.
totalProgressView.setProgress(45, animated: true)
Is there a way to sync the animation stroke based on the progress of the UIProgressView?

I thought a UIProgressView took a progress value from 0 to 1? If so, your sample code that sets the progress view to 45 doesn't make sense.
UIProgressView and a shape layer's strokeEnd both use values from 0 to 1, so you should be able to just set both the progress view and your custom shape layer's strokeEnd to the same fractional value and get them both to show the same amount of completion.

Related

iOS: Increase animation by click a button

I have the following animation, I need to implement a code, by clicking on a button the animation increased by one until a value that I want to reach it.
I try the following code:
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
override func viewDidLoad() {
drawBgShape()
drawTimeLeftShape()
}
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.white.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
view.layer.addSublayer(timeLeftShapeLayer)
}
and when click a button :
var x = 0.0
#IBAction func click(_ sender: Any) {
strokeIt.fromValue = x
strokeIt.toValue = x + 0.1
strokeIt.duration = 0.25
timeLeftShapeLayer.add(strokeIt, forKey: nil)
x = x + 0.1
}
How can I achieve that?
You don't need to use an explicit animation to achieve this.
For some properties of CALayer, Core Animation will add an implicit animation for you. The strokeEnd property is one of these.
This means that Core Animation will automatically animate any changes to a layer's strokeEnd property. You don't need to create an animation object and add it yourself.
There are a few things you'll need to amend with your implementation to get it working.
1) The background circle, to look like the image you've posted, should have a grey strokeColor within your drawBgShape function:
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
2) The strokeEnd for timeLeftShapeLayer should be set to 0.0 as its starting value within your drawTimeLeftShape function.
timeLeftShapeLayer.strokeEnd = 0.0
3) Remove the strokeIt animation property from your class, you don't need it.
4) Update your click function to slowly increment the strokeEnd property of timeLeftShapeLayer directly. This is one simple line of code:
timeLeftShapeLayer.strokeEnd += 0.1
Here's an example implementation:
class ViewController: UIViewController {
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
drawBgShape()
drawTimeLeftShape()
}
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY),
radius: 100,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY),
radius: 100,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
timeLeftShapeLayer.strokeEnd = 0.0
view.layer.addSublayer(timeLeftShapeLayer)
}
#IBAction func click(_ sender: Any) {
timeLeftShapeLayer.strokeEnd += 0.1
}
}

Creating a thin black circle (unfilled) within a filled white circle (UIButton)

I'm trying to replicate the default camera button on iOS devices:
I'm able to create a white circular button with black button within it. However, the black button is also filled, instead of just being a thin circle.
This is what I have (most of it has been copied from different sources and put together, so the code isn't efficient)
The object represents the button,
func applyRoundCorner(_ object: AnyObject) {
//object.layer.shadowColor = UIColor.black.cgColor
//object.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
//object.layer.shadowRadius = 1.0
//object.layer.shadowOpacity = 0.5
var CircleLayer = CAShapeLayer()
let center = CGPoint (x: object.frame.size.width / 2, y: object.frame.size.height / 2)
let circleRadius = object.frame.size.width / 6
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI * 2), clockwise: true)
CircleLayer.path = circlePath.cgPath
CircleLayer.strokeColor = UIColor.black.cgColor
//CircleLayer.fillColor = UIColor.blue.cgColor
CircleLayer.lineWidth = 1
CircleLayer.strokeStart = 0
CircleLayer.strokeEnd = 1
object.layer.addSublayer(CircleLayer)
}
Basic Approach
You could do it like this (for the purpose of demonstration, I would do the button programmatically, using a playground):
let buttonWidth = 100.0
let button = UIButton(frame: CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth))
button.backgroundColor = .white
button.layer.cornerRadius = button.frame.width / 2
Drawing Part:
So, after adding the button and do the desired setup (make it circular), here is part of how you could draw a circle in it:
let circlePath = UIBezierPath(arcCenter: CGPoint(x: buttonWidth / 2,y: buttonWidth / 2), radius: 40.0, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.lineWidth = 2.5
// adding the layer into the button:
button.layer.addSublayer(circleLayer)
Probably, circleLayer.fillColor = UIColor.clear.cgColor is the part you missing 🙂.
Therefore:
Back to your case:
Aside Bar Tip:
For implementing applyRoundCorner, I would suggest to let it has only the job for rounding the view, and then create another function to add the circle inside the view. And that's for avoiding any naming conflict, which means that when reading "applyRoundCorner" I would not assume that it is also would add circle to my view! So:
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
and now:
applyRoundedCorners(for: button)
drawCircle(in: button)
That's seems to be better. From another aspect, consider that you want to make a view to be circular without add a circle in it, with separated methods you could simply applyRoundedCorners(for: myView) without the necessary of adding a circle in it.
Furthermore:
As you can see, I changed AnyObject to UIView, it seems to be more logical to your case. So here is a cool thing that we could do:
extension UIView {
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
}
Now both applyRoundedCorners and drawCircle are implicitly included to the UIView (which means UIButton), instead of passing the button to these functions, you would be able to:
button.applyRoundedCorners()
button.drawCircle()
You just need to add circle Shape layer with lesser width and height
Try this code
func applyRoundCorner(_ object: UIButton) {
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
let anotherFrame = CGRect(x: 12, y: 12, width: object.bounds.width - 24, height: object.bounds.height - 24)
let circle = CAShapeLayer()
let path = UIBezierPath(arcCenter: object.center, radius: anotherFrame.width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
circle.path = path.cgPath
circle.strokeColor = UIColor.black.cgColor
circle.lineWidth = 1.0
circle.fillColor = UIColor.clear.cgColor
object.layer.addSublayer(circle)
}
Note: Change frame value according to your requirements and best user experience
Output
I have no doubt there are a million different ways to approach this problem, this is just one...
I started with a UIButton for simplicity and speed, I might consider actually starting with a UIImage and simply setting the image properties of the button, but it would depend a lot on what I'm trying to achieve
internal extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class RoundButton: UIButton {
override func draw(_ rect: CGRect) {
makeButtonImage()?.draw(at: CGPoint(x: 0, y: 0))
}
func makeButtonImage() -> UIImage? {
let size = bounds.size
UIGraphicsBeginImageContext(CGSize(width: size.width, height: size.height))
defer {
UIGraphicsEndImageContext()
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
// Want to "over fill" the image area, so the mask can be applied
// to the entire image
let radius = min(size.width / 2.0, size.height / 2.0)
let innerRadius = radius * 0.75
let innerCircle = UIBezierPath(arcCenter: center,
radius: innerRadius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
// The color doesn't matter, only it's alpha level
UIColor.red.setStroke()
innerCircle.lineWidth = 4.0
innerCircle.stroke(with: .normal, alpha: 1.0)
let circle = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
UIColor.clear.setFill()
ctx.fill(bounds)
UIColor.white.setFill()
circle.fill(with: .sourceOut, alpha: 1.0)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
nb: This is unoptimised! I would consider caching the result of makeButtonImage and invalidate it when the state/size of the button changes, just beware of that
Why is this approach any "better" then any other? I just want to say, it's not, but what it does create, is a "cut out" of the inner circle
It's a nitpick on my part, but I think it looks WAY better and is a more flexible solution, as you don't "need" a inner circle stroke color, blah, blah, blah
The solution makes use of the CoreGraphics CGBlendModes
Of course I might just do the whole thing in PaintCodeApp and be done with it

I want to Rotate 3 different images on different location in 360 degree in swift 4 using UIBezierPath

I want to Rotate 3 different images on different location in 360 degree in swift 4 using UIBezierPath. i am able to move single image like this in image. with this code
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 120, startAngle: 0, endAngle:CGFloat(Double.pi)*2, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position");
animation.duration = 5
animation.repeatCount = MAXFLOAT
animation.path = circlePath.cgPath
let moon = UIImageView()
moon.frame = CGRect(x:0, y:0, width:40, height:40);
moon.image = UIImage(named: "moon.png")
view.addSubview(moon)
moon.layer.add(animation, forKey: nil)
}
}
and I want to rotate 3 different images on different Angle and Position like this.i want to rotate all 3 different images like in this
Anyone here who can update my code according to the image above I want Thank You in Advance Cheers! :) .
I tried with your code and its working. I made a function to rotate a imageView.
func startMovingAView(startAnlge: CGFloat, endAngle: CGFloat) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 120, startAngle: startAnlge, endAngle:endAngle, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position");
animation.duration = 5
animation.repeatCount = MAXFLOAT
animation.path = circlePath.cgPath
let moon = UIImageView()
moon.frame = CGRect(x:0, y:0, width:40, height:40);
moon.image = UIImage(named: "moon.png")
view.addSubview(moon)
moon.layer.add(animation, forKey: nil)
}
And here is the angles what I pass for each imageView:
/// For first imageView
var startAngle: CGFloat = 0.0
var endAngle: CGFloat = CGFloat(Double.pi) * 2.0
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
/// For second imageView
startAngle = CGFloat(Double.pi/2.0)
endAngle = CGFloat(Double.pi) * 2.5
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
/// For third imageView
startAngle = CGFloat(Double.pi)
endAngle = CGFloat(Double.pi) * 3.0
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
We just need to find the angles with same difference so that in 5 seconds duration the imageView have to travel the same distance so all objects moves with constant and same speed.

Adding a CAShapeLayer around a button

I created a CAShapeLayer in the shape of a circle, and I want to add it around I button i have in the view. I am doing this instead of a border, due to animation purposes. I don't want a border around the button, I'd rather have a shape. This is how I am adding it, but for some reason, it is not adding the shape directly around the button.
This is my code to add the layer
recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.white.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 5
view.layer.addSublayer(recordLine)
This is how it is adding the line for some reason.
This is happening because you are adding Shape layer before rendering the autolayout Constrain properly.
Please add a single line before adding shape layer : self.view.layoutIfNeeded()
#IBOutlet weak var roundButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
roundButton.layer.cornerRadius = 50.0
roundButton.clipsToBounds = true
roundButton.alpha = 0.5
self.view.layoutIfNeeded()
let recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: roundButton.center, radius: roundButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: false)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.black.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 10
view.layer.addSublayer(recordLine)
}
Please check the reference image
This is working for me.
My button is programmatic. I had to add the the cashapelayer in viewDidLayoutSubviews
lazy var cameraButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.lightGray
return button
}()
var wasCAShapeLayerAddedToCameraButton = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wasCAShapeLayerAddedToCameraButton {
wasCAShapeLayerAddedToCameraButton = true
cameraButton.layer.cornerRadius = cameraButton.frame.width / 2
addCAShapeLayerToCameraButton()
}
}
func addCAShapeLayerToCameraButton() {
let circularPath = UIBezierPath(arcCenter: cameraButton.center,
radius: (cameraButton.frame.width / 2),
startAngle: 0,
endAngle: 2 * .pi,
clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 10
view.layer.addSublayer(shapeLayer)
}
// try like this i hope it will work for you.
Note: Color and other properties change as your requirement
let shapeLayer = CAShapeLayer()
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.name = "Star"
let path: CGPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = path
shapeLayer.lineWidth = 5.0
self.layer.mask = shapeLayer

How are these animations typically done in Swift / iOS - See image

What do I need to learn in order to create similar animations to the ones shown in the following picture?
Can some one list all of the technologies involved and possibly a quick process as to how this is done?
For First animation you can use CAShapeLayer and CABasic Animation and animate key path strokeEnd
I builded exactly same you can have look at this link,download and see option Fill circle animation https://github.com/ajaykumar21091/AwesomeCustomAnimations-iOS/tree/master/SimpleCustomAnimations
Edit -
The basic idea here is to draw circle using bezeir path and animate the shapeLayer using CABasicAnimation using keyPath strokeEnd.
override func drawRect(rect: CGRect) {
self.drawBezierWithStrokeColor(circleColor.CGColor,
startAngle: 0,
endAngle: 360,
animated: false)
self.drawBezierWithStrokeColor(self.fillColor.CGColor,
startAngle: 0,
endAngle: (((2*CGFloat(M_PI))/100) * CGFloat(percentage)),
animated: true)
}
//helper methods.
private func drawBezierWithStrokeColor(color:CGColor, startAngle:CGFloat, endAngle:CGFloat, animated:Bool) {
let bezier:CAShapeLayer = CAShapeLayer()
bezier.path = bezierPathWithStartAngle(startAngle, endAngle: endAngle).CGPath
bezier.strokeColor = color
bezier.fillColor = UIColor.clearColor().CGColor
bezier.lineWidth = bounds.width * 0.18
self.layer.addSublayer(bezier)
if (animated) {
let animatedStrokeEnd:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd");
animatedStrokeEnd.duration = (2.0/100)*Double(percentage);
animatedStrokeEnd.fromValue = NSNumber(float: 0.0);
animatedStrokeEnd.toValue = NSNumber(float: 1.0);
animatedStrokeEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
bezier.addAnimation(animatedStrokeEnd, forKey: "strokeEndAnimation");
}
}
private func bezierPathWithStartAngle(startAngle:CGFloat, endAngle:CGFloat) -> UIBezierPath {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius = max(bounds.width, bounds.height)
let arcWidth = bounds.width * 0.25
let path = UIBezierPath(arcCenter : center,
radius : radius/2 - arcWidth/2,
startAngle : startAngle,
endAngle : endAngle ,
clockwise : true)
return path
}

Resources