CAShapeLayer and CABasicAnimation for circle - loader style - blinking - ios

I'm having fun with CAShapeLayer on a playground project.
I want to create an animation coloring the stroke of a CALayer in a clock way, then, an animation removing the stroke color, in the same direction. So it gives the effect to putting and removing the strokes.
So far, it looks pretty much like what in wanted except, just before calling fullAnimate after the second time, the stroke blink like it was "full" ,then it disappears, then the animation plays again.
I agree my code ain't the best so far since I'm just playing with it yet, but I've look for explanation and did not found any usefull answer.
Can someone explain to me what's happening there ? and how to avoid it ?
here is my playground file
import UIKit
import PlaygroundSupport
enum CircleProgressionViewAnimationState {
case start, firstAnimation, secondAnimation, progress, stop
}
class CircleProgressionView : UIView {
static let offset: CGFloat = 10.0
private var path : UIBezierPath? {
didSet {
circleLayer.path = path?.cgPath
}
}
private var state : CircleProgressionViewAnimationState {
didSet {
observe(change: state)
}
}
private var progressionPath : UIBezierPath?
var circleLayerContainer = CALayer()
var circleLayer : CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 4
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.6666666865, green: 0.6666666865, blue: 0.6666666865, alpha: 1)
return shapeLayer
}()
var circleProgressLayer : CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 15
shapeLayer.cornerRadius = 2
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
shapeLayer.lineCap = CAShapeLayerLineCap.round
return shapeLayer
}()
override init(frame: CGRect) {
state = .stop
super.init(frame:frame)
initLayerValues()
}
required init?(coder aDecoder: NSCoder) {
state = .stop
super.init(coder:aDecoder)
initLayerValues()
}
private func initLayerValues() {
let side = min(frame.width, frame.height)
circleLayerContainer.frame = CGRect(x: 0, y: 0, width: side, height: side)
let offset = CircleProgressionView.offset
let bezierSide = side - (offset * 2)
let bezierRect = CGRect(x:offset,
y:offset,
width: bezierSide,
height:bezierSide)
path = UIBezierPath(roundedRect: bezierRect,
cornerRadius: CGFloat(bezierSide / 2))
circleLayerContainer.addSublayer(circleLayer)
layer.addSublayer(circleLayerContainer)
layer.addSublayer(circleProgressLayer)
}
func setProgressionPath(_ progressionInPercent: CGFloat) {
let progression = progressionInPercent / 100 * (360)
let rad = (progression + 270) * CGFloat.pi / 180
let start = 270 * CGFloat.pi / 180
let offset = CircleProgressionView.offset
progressionPath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
radius: (self.frame.size.height - offset - circleProgressLayer.lineWidth / 2) / 2,
startAngle: start,
endAngle: rad,
clockwise: true)
}
func observe(change: CircleProgressionViewAnimationState) {
print(change)
switch change {
case .firstAnimation:
fullAnimate { self.state = .secondAnimation }
break
case .secondAnimation:
emptyAnimate { self.state = .firstAnimation }
break
case .start, .progress, .stop: break
}
}
func animate(loop: Bool) {
var completion : ()->Void = {}
if loop {
state = .start
completion = { self.state = .secondAnimation }
}
fullAnimate(completion: completion)
}
func fullAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
circleProgressLayer.path
CATransaction.begin()
circleProgressLayer.path = progressionPath?.cgPath
CATransaction.setCompletionBlock{ completion() }
let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
CATransaction.commit()
}
func emptyAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
CATransaction.begin()
circleProgressLayer.path = progressionPath?.reversing().cgPath
CATransaction.setCompletionBlock{ completion() }
let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.fromValue = 1.0
animation.toValue = 0.0
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name:
CAMediaTimingFunctionName.easeInEaseOut)
circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
CATransaction.commit()
}
}
var container : UIView = {
let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
let view = UIView(frame: frame)
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
return view
}()
let circle = CircleProgressionView(frame: container.frame)
PlaygroundPage.current.liveView = container
circle.setProgressionPath(100)
container.addSubview(circle)
circle.animate(loop: true)

In the empty animation block add the following two lines and you can figure out the reason easily.
func emptyAnimate(completion: #escaping ()->Void) {
self.state = .progress
circleProgressLayer.removeAllAnimations()
CATransaction.begin()
. .....
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
......
}

Related

why isn't the CAShapeLayer showing up sometimes?

I am using the following custom UIView class as a loading indicator in my app. Most of the time it works great and I can just uses loadingView.startLoading() to make it appear. However, in rare cases, only the UIView appears with a shadow behind it, and the CAShapeLayer is nowhere to be seen. What could be preventing the CAShapeLayer from appearing on top of the LoadingView UIView?
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
self.layer.addSublayer(circleLayer)
calculateCirclePath()
animateCircle(duration: 1, repeats: true)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
private func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
}
func stopLoading() {
loadingView.isHidden = true
}
}
Because a sublayer and animation can't happen in one go, I changed the code to what is shown below and now it works perfectly!
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
layer.cornerRadius = size/8
self.layer.addSublayer(circleLayer)
calculateCirclePath()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
loadingView.animateCircle(duration: 1, repeats: true)
}
func stopLoading() {
loadingView.isHidden = true
}
}
its better practice to call this method in main thread using dispatchQueue as sometime control may be in background thread so it must get returned to main thread before performing any UI updates

How to create a rotating rainbow color circle in iOS

From stackoverflow i got a code for drawing rainbow color circle.But as part of requirement ,I need that circle to be rotated continously ,like a rotating progress loader.Below is the code used for creating Rainbow color circle.
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
// //Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
Please anyone help me in rotating this circle.
Please find below the circle generated with above code:
What you got looks completely overcomplicated in the first place. Take a look at the following example:
class ViewController: UIViewController {
class RainbowView: UIView {
var segmentCount: Int = 10 {
didSet {
refresh()
}
}
var lineWidth: CGFloat = 10 {
didSet {
refresh()
}
}
override var frame: CGRect {
didSet {
refresh()
}
}
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
private var currentGradientLayer: CAGradientLayer?
private func refresh() {
currentGradientLayer?.removeFromSuperlayer()
guard segmentCount > 0 else { return }
currentGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.type = .conic
let colors: [UIColor] = {
var colors: [UIColor] = [UIColor]()
for i in 0..<segmentCount {
colors.append(UIColor(hue: CGFloat(i)/CGFloat(segmentCount), saturation: 1, brightness: 1, alpha: 1))
}
colors.append(UIColor(hue: 0.0, saturation: 1, brightness: 1, alpha: 1)) // Append start color at the end as well to complete the circle
return colors;
}()
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
gradientLayer.mask = {
let shapeLayer = CAShapeLayer()
shapeLayer.frame = bounds
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = UIBezierPath(ovalIn: bounds.inset(by: UIEdgeInsets(top: lineWidth*0.5, left: lineWidth*0.5, bottom: lineWidth*0.5, right: lineWidth*0.5))).cgPath
return shapeLayer
}()
return gradientLayer
}()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview({
let view = RainbowView(frame: CGRect(x: 50.0, y: 100.0, width: 100.0, height: 100.0))
var angle: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { _ in
angle += 0.01
view.transform = CGAffineTransform(rotationAngle: angle)
})
return view
}())
}
}
So a view is generated that uses a conical gradient with mask to draw the circle you are describing. Then a transform is applied to the view to rotate it. And a Timer is scheduled to rotate the circle.
Note that this code will leak because timer is nowhere invalidated. It needs to be removed when view disappears or similar.
The easiest way would be to attach an animation that repeats forever:
let animation = CABasicAnimation(keyPath: "transform.rotation") // Create rotation animation
animation.repeatCount = .greatestFiniteMagnitude // Repeat animation for as long as we can
animation.fromValue = 0 // Rotate from 0
animation.toValue = 2 * Float.pi // to 360 deg
animation.duration = 1 // During 1 second
self.layer.add(animation, forKey: "animation") // Adding the animation to the view
self - is RainbowCircle, assuming that you add this code to one of the methods inside it.
For this we can have Image something like this
syncImage.image = UIImage(named:"spinning")
Create a below extension to Start/Stop Rotating
extension UIView {
// To animate
func startRotating(duration: Double = 1) {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) == nil {
let animate = CABasicAnimation(keyPath: "transform.rotation")
animate.duration = duration
animate.repeatCount = Float.infinity
animate.fromValue = 0.0
animate.toValue = Float(M_PI * 2.0)
self.layer.addAnimation(animate, forKey: kAnimationKey)
}
}
func stopRotating() {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) != nil {
self.layer.removeAnimationForKey(kAnimationKey)
}
}
}
Usage
func startSpinning() {
syncImage.startRotating()
}
func stopSpinning() {
syncImage.stopRotating()
}
func handleSyncTap(sender: UITapGestureRecognizer? = nil) {
startSpinning()
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.stopSpinning()
})
}

Custom indicator with rotate blink animation like UIActivityIndicatorView

I am trying to make custom activity indicator, see the indicator class below
import UIKit
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
var count = 0
override func draw(_ rect: CGRect) {
super.draw(rect)
}
func blink() {
backgroundColor = .clear
let duration: CFTimeInterval = 1.2
//let beginTime = CACurrentMediaTime()
let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5]
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Animation
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.keyTimes = [0, 0.5, 1]
animation.timingFunctions = [timingFunction, timingFunction]
animation.values = [1, 0.3, 1]
animation.duration = duration
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false
for i in 0...3 {
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 6.8
shape.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2
shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
animation.beginTime = beginTimes[i]
shape.add(animation, forKey: "animation")
self.layer.addSublayer(shape)
}
}
func startAnimating() {
blink()
}
}
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(indicator)
indicator.startAnimating()
I have attached my current result.
But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.
Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
private var replicatorLayer = CAReplicatorLayer()
private var mainShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
mainShapeLayer = CAShapeLayer()
mainShapeLayer.frame = self.bounds
mainShapeLayer.fillColor = UIColor.clear.cgColor
mainShapeLayer.lineWidth = 6.8
mainShapeLayer.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2
mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = self.bounds
replicatorLayer.instanceCount = 4
let angle = (Double.pi * 2)/4
replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1)
replicatorLayer.addSublayer(mainShapeLayer)
replicatorLayer.opacity = 0
self.layer.addSublayer(replicatorLayer)
}
func animate(){
let defaultDuration : Double = 0.75
let animate = CAKeyframeAnimation(keyPath: "opacity")
animate.values = [1, 0.3, 1]
animate.keyTimes = [0, 0.5, 1]
animate.repeatCount = .greatestFiniteMagnitude
animate.duration = defaultDuration
animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicatorLayer.instanceDelay = defaultDuration/4
self.mainShapeLayer.add(animate, forKey: nil)
let opacityIn = CABasicAnimation(keyPath: "opacity")
opacityIn.fromValue = 1
opacityIn.toValue = 0
opacityIn.duration = 0.2
replicatorLayer.add(opacityIn, forKey: nil)
self.replicatorLayer.opacity = 1
}
func stopAnimating(){
CATransaction.begin()
let opacityOut = CABasicAnimation(keyPath: "opacity")
opacityOut.fromValue = 1
opacityOut.toValue = 0
opacityOut.duration = 0.2
CATransaction.setCompletionBlock {
[weak self] in
self?.mainShapeLayer.removeAllAnimations()
}
replicatorLayer.add(opacityOut, forKey: nil)
self.replicatorLayer.opacity = 0
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
mainShapeLayer.frame = self.bounds
replicatorLayer.frame = self.bounds
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
indicator.animate()
//just to simulate starting and stoping
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
indicator.stopAnimating()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
indicator.animate()
}
}
view.addSubview(indicator)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

UIView horizontal bar animation in swift

I am working on this animation where a number will be received every second and progress bar has to fill or go down based on the double value.
I have created the views and have added all the views in the UIStackView. Also made the outlet collection for all the views. (sorting them by the tag and making them round rect).
I can loop the views and change their background color but trying to see if there is a better way to do it. Any suggestions?
Thanks
So how you are doing it is fine. Here would be two different ways. The first with Core Graphics. You may want to update methods and even make the color gradient in the sublayer.
1st Way
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
var fillReversed = false
override init(frame: CGRect) {
super.init(frame: frame)
setUp(animated: false)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp(animated: false)
backgroundColor = UIColor.green
}
func updateProgress(progress:Double, animated:Bool) {
currentProgress = progress
setUp(animated: animated)
}
private func setUp(animated:Bool){
// internal space
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
if animated == true{
let trans = CATransition()
trans.type = kCATransitionFade
trans.duration = 0.5
self.layer.add(trans, forKey: nil)
}
layer.sublayers?.removeAll()
for i in 0...Int(indicators - 1.0){
let indicatorLayer = CALayer()
indicatorLayer.frame = CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height)
//haha i don't understand my logic below but it works hahaha
// i know it has to go backwards
if fillReversed{
if CGFloat(1 - currentProgress) * self.bounds.height < indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}
}else{
if CGFloat(currentProgress) * self.bounds.height > indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}
}
indicatorLayer.cornerRadius = indicatorLayer.frame.height * radiusFactor
indicatorLayer.masksToBounds = true
self.layer.addSublayer(indicatorLayer)
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
setUp(animated: false)
}
}
The second way is using CAShapeLayer and the benefit is that the progress will get a natural animation.
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
private var progressLayer : CALayer?
private var shapeHoles : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
transparentDotsAndProgress()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
transparentDotsAndProgress()
}
func updateProgress(progress:Double) {
if progress <= 1 && progress >= 0{
currentProgress = progress
transparentDotsAndProgress()
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
transparentDotsAndProgress()
}
func transparentDotsAndProgress(){
self.layer.masksToBounds = true
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height), cornerRadius: 0)
path.usesEvenOddFillRule = true
for i in 0...Int(indicators - 1.0){
let circlePath = UIBezierPath(roundedRect: CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height), cornerRadius: height * radiusFactor)
path.append(circlePath)
}
if progressLayer == nil{
progressLayer = CALayer()
progressLayer?.backgroundColor = filledIndicatorColor.cgColor
self.layer.addSublayer(progressLayer!)
}
progressLayer?.frame = CGRect(x: 0, y: -self.bounds.height - padding + CGFloat(currentProgress) * self.bounds.height, width: bounds.width, height: bounds.height)
self.shapeHoles?.removeFromSuperlayer()
shapeHoles = CAShapeLayer()
shapeHoles?.path = path.cgPath
shapeHoles?.fillRule = kCAFillRuleEvenOdd
shapeHoles?.fillColor = UIColor.white.cgColor
shapeHoles?.strokeColor = UIColor.clear.cgColor
self.layer.backgroundColor = indicatorColor.cgColor
self.layer.addSublayer(shapeHoles!)
}
}
Both of these ways should work but the advantage of the CAShapeLayer is you get a natural animation.
I'm a firm believer in learning through solving organic problems and slowly building my global knowledge on a subject. So I'm afraid I don't have any good tutorials for you.
Here is an example that will jump start you, though.
// For participating in Simulator's "slow animations"
#_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
import UIKit
#IBDesignable
class VerticalProgessView: UIControl {
#IBInspectable
var numberOfSegments: UInt = 0
#IBInspectable
var verticalSegmentGap: CGFloat = 4.0
#IBInspectable
var outerColor: UIColor = UIColor(red: 33, green: 133, blue: 109)
#IBInspectable
var unfilledColor: UIColor = UIColor(red: 61, green: 202, blue: 169)
#IBInspectable
var filledColor: UIColor = UIColor.white
private var _progress: Float = 0.25
#IBInspectable
open var progress: Float {
get {
return _progress
}
set {
self.setProgress(newValue, animated: false)
}
}
let progressLayer = CALayer()
let maskLayer = CAShapeLayer()
var skipLayoutSubviews = false
open func setProgress(_ progress: Float, animated: Bool) {
if progress < 0 {
_progress = 0
} else if progress > 1.0 {
_progress = 1
} else {
// Clamp the percentage to discreet values
let discreetPercentageDistance: Float = 1.0 / 28.0
let nearestProgress = discreetPercentageDistance * round(progress/discreetPercentageDistance)
_progress = nearestProgress
}
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
self?.skipLayoutSubviews = false
}
if !animated {
CATransaction.setDisableActions(true)
} else {
CATransaction.setAnimationDuration(0.25 * Double(UIAnimationDragCoefficient()))
}
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
skipLayoutSubviews = true
CATransaction.commit() // This triggers layoutSubviews
}
override func prepareForInterfaceBuilder() {
awakeFromNib()
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
self.layer.backgroundColor = unfilledColor.cgColor
// Initialize and add the progressLayer
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
progressLayer.backgroundColor = filledColor.cgColor
self.layer.addSublayer(progressLayer)
// Initialize and add the maskLayer (it has the holes)
maskLayer.frame = self.layer.bounds
maskLayer.fillColor = outerColor.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = maskPath(for: maskLayer.bounds)
self.layer.addSublayer(maskLayer)
// Layer hierarchy
// self.maskLayer
// self.progressLayer
// self.layer
}
override func layoutSubviews() {
super.layoutSubviews()
if skipLayoutSubviews {
// Crude but effective, not fool proof though
skipLayoutSubviews = false
return
}
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Doesn't work for 180° rotations
let duration = UIApplication.shared.statusBarOrientationAnimationDuration * Double(UIAnimationDragCoefficient())
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
CATransaction.setAnimationDuration(duration)
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
let size = self.bounds.size
let anchorPoint = CGPoint(x: 0.5, y: 1.0)
maskLayer.anchorPoint = anchorPoint
maskLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
maskLayer.position = CGPoint(x: size.width * anchorPoint.x, y: size.height * anchorPoint.y)
// Animate the segments
let pathChangeAnimation = CAKeyframeAnimation(keyPath: "path")
let finalPath = maskPath(for: maskLayer.bounds)
pathChangeAnimation.values = [maskLayer.path!, finalPath]
pathChangeAnimation.keyTimes = [0, 1]
pathChangeAnimation.timingFunction = timingFunction
pathChangeAnimation.duration = duration
pathChangeAnimation.isRemovedOnCompletion = true
maskLayer.add(pathChangeAnimation, forKey: "pathAnimation")
CATransaction.setCompletionBlock {
// CAAnimation's don't actually change the value
self.maskLayer.path = finalPath
}
CATransaction.commit()
}
// Provides a path that will mask out all the holes to show self.layer and the progressLayer behind
private func maskPath(for rect: CGRect) -> CGPath {
let horizontalSegmentGap: CGFloat = 5.0
let segmentWidth = rect.width - horizontalSegmentGap * 2
let segmentHeight = rect.height/CGFloat(numberOfSegments) - verticalSegmentGap + verticalSegmentGap/CGFloat(numberOfSegments)
let segmentSize = CGSize(width: segmentWidth, height: segmentHeight)
let segmentRect = CGRect(x: horizontalSegmentGap, y: 0, width: segmentSize.width, height: segmentSize.height)
let path = CGMutablePath()
for i in 0..<numberOfSegments {
// Literally, just move it down by the y value here
// this simplifies the math of calculating the starting points and what not
let transform = CGAffineTransform.identity.translatedBy(x: 0, y: (segmentSize.height + verticalSegmentGap) * CGFloat(i))
let segmentPath = UIBezierPath(roundedRect: segmentRect, cornerRadius: segmentSize.height / 2)
segmentPath.apply(transform)
path.addPath(segmentPath.cgPath)
}
// Without the outerPath, we'll end up with a bunch of squircles instead of a bunch of holes
let outerPath = CGPath(rect: rect, transform: nil)
path.addPath(outerPath)
return path
}
/// Provides the current and correct bounds and position for the progressLayer
private func progressLayerProperties() -> (bounds: CGRect, position: CGPoint) {
let frame = self.bounds
let height = frame.height * CGFloat(progress)
let y = frame.height * CGFloat(1 - progress)
let width = frame.width
let anchorPoint = maskLayer.anchorPoint
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let position = CGPoint(x: 0 + width * anchorPoint.x, y: y + height * anchorPoint.x)
return (bounds: bounds, position: position)
}
// TODO: Implement functions to further mimic UIProgressView
}
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
}
}
Using in a storyboard
Enjoy the magic

My indicator is blank [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I use a custom indicator but when i call the subclass indicator in my viewdidload my view controller is blank but when i run it in a playground i can see it in the side window. Here is the code of the indicator. Theres no error but my indicator is not showing. Thats my problem. I would appreciate it if someone could tell me why.
import UIKit
class MaterialLoadingIndicator: UIView {
let MinStrokeLength: CGFloat = 0.05
let MaxStrokeLength: CGFloat = 0.7
let circleShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
initShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func initShapeLayer() {
circleShapeLayer.actions = ["strokeEnd" : NSNull(),
"strokeStart" : NSNull(),
"transform" : NSNull(),
"strokeColor" : NSNull()]
circleShapeLayer.backgroundColor = UIColor.clear.cgColor
circleShapeLayer.strokeColor = UIColor.blue.cgColor
circleShapeLayer.fillColor = UIColor.clear.cgColor
circleShapeLayer.lineWidth = 5
circleShapeLayer.lineCap = kCALineCapRound
circleShapeLayer.strokeStart = 0
circleShapeLayer.strokeEnd = MinStrokeLength
let center = CGPoint(x: bounds.width*0.5, y: bounds.height*0.5)
circleShapeLayer.frame = bounds
circleShapeLayer.path = UIBezierPath(arcCenter: center,
radius: center.x,
startAngle: 0,
endAngle: CGFloat(M_PI*2),
clockwise: true).cgPath
layer.addSublayer(circleShapeLayer)
}
func startAnimating() {
if layer.animation(forKey: "rotation") == nil {
startColorAnimation()
startStrokeAnimation()
startRotatingAnimation()
}
}
private func startColorAnimation() {
let color = CAKeyframeAnimation(keyPath: "strokeColor")
color.duration = 10.0
color.values = [UIColor(hex: 0x4285F4, alpha: 1.0).cgColor,
UIColor(hex: 0xDE3E35, alpha: 1.0).cgColor,
UIColor(hex: 0xF7C223, alpha: 1.0).cgColor,
UIColor(hex: 0x1B9A59, alpha: 1.0).cgColor,
UIColor(hex: 0x4285F4, alpha: 1.0).cgColor]
color.calculationMode = kCAAnimationPaced
color.repeatCount = Float.infinity
circleShapeLayer.add(color, forKey: "color")
}
private func startRotatingAnimation() {
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = M_PI*2
rotation.duration = 2.2
rotation.isCumulative = true
rotation.isAdditive = true
rotation.repeatCount = Float.infinity
layer.add(rotation, forKey: "rotation")
}
private func startStrokeAnimation() {
let easeInOutSineTimingFunc = CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1.0)
let progress: CGFloat = MaxStrokeLength
let endFromValue: CGFloat = circleShapeLayer.strokeEnd
let endToValue: CGFloat = endFromValue + progress
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
strokeEnd.fromValue = endFromValue
strokeEnd.toValue = endToValue
strokeEnd.duration = 0.5
strokeEnd.fillMode = kCAFillModeForwards
strokeEnd.timingFunction = easeInOutSineTimingFunc
strokeEnd.beginTime = 0.1
strokeEnd.isRemovedOnCompletion = false
let startFromValue: CGFloat = circleShapeLayer.strokeStart
let startToValue: CGFloat = fabs(endToValue - MinStrokeLength)
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
strokeStart.fromValue = startFromValue
strokeStart.toValue = startToValue
strokeStart.duration = 0.4
strokeStart.fillMode = kCAFillModeForwards
strokeStart.timingFunction = easeInOutSineTimingFunc
strokeStart.beginTime = strokeEnd.beginTime + strokeEnd.duration + 0.2
strokeStart.isRemovedOnCompletion = false
let pathAnim = CAAnimationGroup()
pathAnim.animations = [strokeEnd, strokeStart]
pathAnim.duration = strokeStart.beginTime + strokeStart.duration
pathAnim.fillMode = kCAFillModeForwards
pathAnim.isRemovedOnCompletion = false
CATransaction.begin()
CATransaction.setCompletionBlock {
if self.circleShapeLayer.animation(forKey: "stroke") != nil {
self.circleShapeLayer.transform = CATransform3DRotate(self.circleShapeLayer.transform, CGFloat(M_PI*2) * progress, 0, 0, 1)
self.circleShapeLayer.removeAnimation(forKey: "stroke")
self.startStrokeAnimation()
}
}
circleShapeLayer.add(pathAnim, forKey: "stroke")
CATransaction.commit()
}
func stopAnimating() {
circleShapeLayer.removeAllAnimations()
layer.removeAllAnimations()
circleShapeLayer.transform = CATransform3DIdentity
layer.transform = CATransform3DIdentity
}
}
extension UIColor {
convenience init(hex: UInt, alpha: CGFloat) {
self.init(
red: CGFloat((hex & 0xFF0000) >> 16) / 255.0,
green: CGFloat((hex & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(hex & 0x0000FF) / 255.0,
alpha: CGFloat(alpha)
)
}
}
And here is the code of my view controller in the viewdidload
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 568))
let indicator = MaterialLoadingIndicator(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
indicator.center = CGPoint(x: 320*0.5, y: 568*0.5)
view.addSubview(indicator)
indicator.startAnimating()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The view holding the indicator is just floating around, feeling lost, feeling unhappy for not belonging to, not being added to someone. :)
EDIT :
Okay John, now that we are stuck, let us add the indicator to someone.
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 568))
let indicator = MaterialLoadingIndicator(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
indicator.center = CGPoint(x: 320*0.5, y: 568*0.5)
view.addSubview(indicator)
indicator.startAnimating()
self.view.addSubview(view) // John, this is what was missing
}

Resources