My indicator is blank [closed] - ios

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
}

Related

CAShapeLayer and CABasicAnimation for circle - loader style - blinking

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
......
}

Centering CAShapeLayer within UIView Swift

I'm having trouble centering CAShapeLayer within a UIView. I've search and most solutions are from pre 2015 in Obj C or haven't been solved.
Attached is what the image looks like. When I inspect it, its inside the red view, but idk why its not centering. I've tried resizing it but still doesn't work.
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
//MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 200, height: 200)
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
setupCircleLayers()
}
var shapeLayer: CAShapeLayer!
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: UIColor.rgb(red: 56, green: 25, blue: 49, alpha: 1), fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
progressView.layer.addSublayer(trackLayer)
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint(x: progressView.frame.width / 2, y: progressView.frame.height / 2)
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = progressView.center
return layer
}
As #ukim says, your problem is that you are trying to determine the position of your layer, based on views and their size before these are finite.
When you are in viewDidLoad you don't know the size and final position of your views yet. You can add the progressView alright but you can not be sure that its size or position are correct until viewDidLayoutSubviews (documented here).
So, if I move your call to setupCircleLayers to viewDidLayoutSubviews and I change the centerpoint to CGPoint.zero and alter the calculation of your layer.position to this:
layer.position = CGPoint(x: progressView.frame.size.width / 2, y: progressView.frame.size.height / 2)
Then I see this:
Which I hope is more what you were aiming for.
Here is the complete listing (note that I had to change some of your methods as I didn't have access to anchor or UIColor.rgb for instance but you can probably work your way around that :))
import UIKit
class ViewController: UIViewController {
var shapeLayer: CAShapeLayer!
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.heightAnchor.constraint(equalToConstant: 200).isActive = true
progressView.widthAnchor.constraint(equalToConstant: 200).isActive = true
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
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))
progressView.layer.addSublayer(trackLayer)
}
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 = progressView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "myLayer"}).count == 0
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint.zero
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = CGPoint(x: progressView.frame.size.width / 2, y: progressView.frame.size.height / 2)
layer.name = "myLayer"
return layer
}
}
Hope that helps.
Caveat
When you do the above, that also means that every time viewDidLayoutSubviews is called, you are adding a new layer. To circumvent that, you can use the name property of a layer
layer.name = "myLayer"
and then check if you have already added your layer. Something like this should work:
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 = progressView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "myLayer"}).count == 0
}
Which you then use here:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
I've updated the listing.
You are calling setupCircleLayers()in viewDidLoad(). At the time, progressView.frame has not been calculated from the constraints yet.
Try
let centerpoint = CGPoint(x: 100, y: 100)
instead of calculating the value from progressView.frame
You can try this in your playground:
import UIKit
import PlaygroundSupport
class MyVC: UIViewController {
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
var layer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.backgroundColor = .red
progressView.widthAnchor.constraint(equalToConstant: 200).isActive = true
progressView.heightAnchor.constraint(equalToConstant: 200).isActive = true
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
setupCircleLayers()
}
var shapeLayer: CAShapeLayer!
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: .red, fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
progressView.layer.addSublayer(trackLayer)
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint(x: 100, y: 100)
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
return layer
}
}
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 375.0, height: 667.0))
let vc = MyVC()
PlaygroundPage.current.liveView = vc.view

How do i call functions of a class - Swift

Im still trying to learn this portion of things. I've looked around and read a few questions about it but truthfully I dont understand any of it.
I've got a circle class that creates and draws a circle
class CircleView: UIView {
var circleLayer: CAShapeLayer!
var isAnimating = false
override init(frame: CGRect) {
let fColor = UIColor.clear.cgColor
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleLayer.lineWidth = 9.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setCircleClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func setCircleCounterClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func formatCirle(circlePath: UIBezierPath) -> CAShapeLayer{
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.clear.cgColor
circleShape.strokeColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1).cgColor
circleShape.lineWidth = 9.0;
circleShape.strokeEnd = 0.0
return circleShape
}
func animate(duration: TimeInterval){
self.isAnimating = true
self.animateCircleFull(duration: 1)
}
func endAnimate(){
self.isAnimating = false
}
func animateCircleFull(duration: TimeInterval) {
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
self.setCircleCounterClockwise()
self.animateCircleEmpty(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
func animateCircleEmpty(duration: TimeInterval){
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1
animation.toValue = 0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 0
CATransaction.setCompletionBlock {
self.setCircleClockwise()
self.animateCircleFull(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
Which is being called from my viewController with the below function. It all works fine but what i cant work out is how do i go about calling the endAnimation function on the same circle?
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
}
For calling endAnimation function on the same circle you have to declare property in your ViewController class.
import UIKit
class ViewController: UIViewController {
var circleView = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
self.addCircleView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(100)
var circleHeight = circleWidth
var bgColor: UIColor = UIColor.init(rgbColorCodeRed: 230, green: 226, blue: 218, alpha: 1)
// Create a new CircleView
self.circleView = CircleView(frame: CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
cv = circleView
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animate(duration: 1)
let imageName = "ButtonBackground.png"
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame = CGRect(x: self.view.frame.width/2, y: self.view.frame.height-110, width: 100, height: 100)
view.addSubview(imageView)
view.bringSubview(toFront: circleView)
//now you are able to call endAnimation function
self.circleView.endAnimate()
}
}

After CAReplicatorLayer animation in a `vc`'s `subview`, switch `vc` comes a strange issue

CAReplicator did not keep the state after the switch vc:
Dots of CAReplicator did not keep its scale after the vc switch back.
As you see, the circle animation is created by CAReplicator.
after the main vc switch to another vc, then switch back, the Circle's dots become very small. witch is set in the initial.
My code is below:
In the main vc:
func initUI() {
let lml_frame = CGRect.init(x: 0, y: 64, width: self.view.bounds.size.width, height: 400)
lml_digtal_view = LMLDigitalDazzleAnimationView.init(frame: lml_frame)
self.view.addSubview(lml_digtal_view!)
}
In the LMLDigitalDazzleAnimationView:
import Foundation
import UIKit
class LMLDigitalDazzleAnimationView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
var initFrame = CGRect.init(x: 0, y: 0, width: 320, height: 480)
var fromColor = UIColor.init(red: 240/255.0, green: 77.0/255.0, blue: 48.0/255.0, alpha: 1.0).cgColor
var toColor = UIColor.init(red: 220.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).cgColor
var money:Float? = 1200.25 {
didSet {
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initFrame = frame
initUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initUI(){
let gradul_layer = CAGradientLayer.init()
gradul_layer.frame = CGRect.init(x: 0, y: 0, width: initFrame.width, height: initFrame.height)
gradul_layer.colors = [
fromColor,
toColor
]
gradul_layer.startPoint = CGPoint.init(x: 0.5, y: 0.3)
gradul_layer.endPoint = CGPoint.init(x: 0.5, y: 0.7)
layer.addSublayer(gradul_layer)
let wave_view0 = KHWaveView.init(frame: CGRect.init(x: 0, y: initFrame.height - 80, width: initFrame.width, height: 80))
//wave_view.backgroundColor = UIColor.white
wave_view0.waveColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 0.5)
wave_view0.waveSpeed = 1.3
wave_view0.waveTime = 0
wave_view0.wave()
self.addSubview(wave_view0)
let wave_view = KHWaveView.init(frame: CGRect.init(x: 0, y: initFrame.height - 80, width: initFrame.width, height: 80))
//wave_view.backgroundColor = UIColor.white
wave_view.waveColor = UIColor.white
wave_view.waveSpeed = 1.0
wave_view.waveTime = 0
wave_view.wave()
self.addSubview(wave_view)
animateCircle()
animateDigitalIcrease(money: money!)
}
func animateCircle() -> Void {
let r = CAReplicatorLayer()
r.bounds = CGRect(x:0.0, y:0.0, width:260.0, height:260.0)
r.cornerRadius = 10.0
r.backgroundColor = UIColor.clear.cgColor
r.position = CGPoint.init(x: self.bounds.width / 2.0, y: 160)
self.layer.addSublayer(r)
let dot = CALayer()
dot.bounds = CGRect(x:0.0, y :0.0, width:6.0, height:6.0)
dot.position = CGPoint(x:100.0, y:10.0)
dot.backgroundColor = UIColor(white:1, alpha:1.0).cgColor
dot.cornerRadius = 3.0
r.addSublayer(dot)
let nrDots: Int = 32
r.instanceCount = nrDots
let angle = CGFloat(2*M_PI) / CGFloat(nrDots)
r.instanceTransform = CATransform3DMakeRotation(angle, 0.1, 0.1, 1.0)
let duration:CFTimeInterval = 1.5
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 1.0 // 0.5
shrink.duration = duration
shrink.repeatCount = Float.infinity
dot.add(shrink, forKey: nil)
r.instanceDelay = duration/Double(nrDots)
dot.transform = CATransform3DMakeScale(0.1, 0.1, 0.1)
delay(delay: duration) {
let turn_key_path = "transform.rotation"
let turn_ani = CABasicAnimation.init(keyPath: turn_key_path)
turn_ani.isRemovedOnCompletion = false
turn_ani.fillMode = kCAFillModeForwards
turn_ani.toValue = M_PI*2
turn_ani.duration = 2.0
turn_ani.repeatCount = 2
r.add(turn_ani, forKey: turn_key_path)
}
}
func delay(delay:Double, closure:#escaping ()->()){
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
func animateDigitalIcrease(money :Float){
let frame = CGRect.init(x: 0, y: 0, width: 120, height: 80)
let counterLabel = LMLDigitalIncreaseLabel.init(frame: frame, andDuration: 2.0, andFromValue: 0, andToValue: money)
counterLabel?.center = CGPoint.init(x: self.bounds.size.width / 2.0, y: 130)
self.addSubview(counterLabel!)
counterLabel?.start()
delay(delay: 5.0) {
counterLabel?.stop()
self.animateFadeShowSmallMoney()
}
}
func animateFadeShowSmallMoney(){
let border_view = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 30))
border_view.layer.cornerRadius = 15
border_view.layer.masksToBounds = true
border_view.layer.borderWidth = 1
border_view.backgroundColor = UIColor.clear
border_view.layer.borderColor = UIColor.white.cgColor
let small_money_frame = CGRect.init(x: 0, y: 0, width: 80, height: 30)
let small_money = UILabel.init(frame: small_money_frame)
small_money.center = border_view.center
small_money.adjustsFontSizeToFitWidth = true
small_money.textAlignment = NSTextAlignment.center
small_money.text = "mo:" + String(format:"%.2f", money!)
small_money.textColor = UIColor.white
border_view.addSubview(small_money)
border_view.alpha = 0.0
self.addSubview(border_view)
border_view.center = CGPoint.init(x: self.bounds.size.width/2.0, y: 220)
UIView.animate(withDuration: 1.0) {
border_view.alpha = 1.0
}
}
}
My code is not good, you can advice me how to encapsulate a animation class better.
After many attention, I solve my issue:
delay(delay: duration) {
let turn_key_path = "transform.rotation"
let turn_ani = CABasicAnimation.init(keyPath: turn_key_path)
turn_ani.isRemovedOnCompletion = false
turn_ani.fillMode = kCAFillModeForwards
turn_ani.toValue = M_PI*2
turn_ani.duration = 2.0
turn_ani.repeatCount = 2
r.add(turn_ani, forKey: turn_key_path)
dot.transform = CATransform3DMakeScale(1, 1, 1) // add this line solve my issue.
}

Swift - Color fill animation

I'm relatively new to ios animations and I believe there's something wrong with the approach I took to animate UIView.
I will start with a UI screenshot to picture my problem more precisely:
There is a tableView cell with two labels and colorful filled circle
Anytime I introduce new value to the cell, I'd like to animate this left-most bulb so it looks like it's getting filled with red color.
This is the implementation od BadgeView, which is basically the aforementioned leftmost filled circle
class BadgeView: UIView {
var coeff:CGFloat = 0.5
override func drawRect(rect: CGRect) {
let topRect:CGRect = CGRectMake(0, 0, rect.size.width, rect.size.height*(1.0 - self.coeff))
UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0).setFill()
UIRectFill(topRect)
let bottomRect:CGRect = CGRectMake(0, rect.size.height*(1-coeff), rect.size.width, rect.size.height*coeff)
UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0).setFill()
UIRectFill(bottomRect)
self.layer.cornerRadius = self.frame.height/2.0
self.layer.masksToBounds = true
}
}
This is the way I achieve uneven fill - I introduced coefficient which I modify in viewController.
Inside my cellForRowAtIndexPath method I try to animate this shape using custom button with callback
let btn:MGSwipeButton = MGSwipeButton(title: "", icon: img, backgroundColor: nil, insets: ins, callback: {
(sender: MGSwipeTableCell!) -> Bool in
print("Convenience callback for swipe buttons!")
UIView.animateWithDuration(4.0, animations:{ () -> Void in
cell.pageBadgeView.coeff = 1.0
let frame:CGRect = cell.pageBadgeView.frame
cell.pageBadgeView.drawRect(frame)
})
return true
})
But it does nothing but prints to console
: CGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
Although I'd love to know the right answer and approach, it would be great to know, for education purpose, why this code doesn't work.
Thanks in advance
The error part of the problem seems to be this part of the code:
cell.pageBadgeView.drawRect(frame)
From the Apple docs on UIView drawRect:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
So if you'd change your code to:
cell.pageBadgeView.setNeedsDisplay()
You'll get rid of the error and see the badgeView filled correctly. However this won't animate it, since drawRect isn't animatable by default.
The easiest workaround to your problem would be for BadgeView to have an internal view for the fill color. I'd refactor the BadgeView as so:
class BadgeView: UIView {
private let fillView = UIView(frame: .zero)
private var coeff:CGFloat = 0.5 {
didSet {
// Make sure the fillView frame is updated everytime the coeff changes
updateFillViewFrame()
}
}
// Only needed if view isn't created in xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// Only needed if view isn't created in xib or storyboard
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
override func awakeFromNib() {
setupView()
}
private func setupView() {
// Setup the layer
layer.cornerRadius = bounds.height/2.0
layer.masksToBounds = true
// Setup the unfilled backgroundColor
backgroundColor = UIColor(red: 249.0/255.0, green: 163.0/255.0, blue: 123.0/255.0, alpha: 1.0)
// Setup filledView backgroundColor and add it as a subview
fillView.backgroundColor = UIColor(red: 252.0/255.0, green: 95.0/255.0, blue: 95.0/255.0, alpha: 1.0)
addSubview(fillView)
// Update fillView frame in case coeff already has a value
updateFillViewFrame()
}
private func updateFillViewFrame() {
fillView.frame = CGRect(x: 0, y: bounds.height*(1-coeff), width: bounds.width, height: bounds.height*coeff)
}
// Setter function to set the coeff animated. If setting it not animated isn't necessary at all, consider removing this func and animate updateFillViewFrame() in coeff didSet
func setCoeff(coeff: CGFloat, animated: Bool) {
if animated {
UIView.animate(withDuration: 4.0, animations:{ () -> Void in
self.coeff = coeff
})
} else {
self.coeff = coeff
}
}
}
In your button callback you'll just have to do:
cell.pageBadgeView.setCoeff(1.0, animated: true)
try this playground code
import UIKit
import CoreGraphics
var str = "Hello, playground"
class BadgeView: UIView {
var coeff:CGFloat = 0.5
func drawCircleInView(){
// Set up the shape of the circle
let size:CGSize = self.bounds.size;
let layer = CALayer();
layer.frame = self.bounds;
layer.backgroundColor = UIColor.blue().cgColor
let initialRect:CGRect = CGRect.init(x: 0, y: size.height, width: size.width, height: 0)
let finalRect:CGRect = CGRect.init(x: 0, y: size.height/2, width: size.width, height: size.height/2)
let sublayer = CALayer()
sublayer.frame = initialRect
sublayer.backgroundColor = UIColor.orange().cgColor
sublayer.opacity = 0.5
let mask:CAShapeLayer = CAShapeLayer()
mask.frame = self.bounds
mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
mask.fillColor = UIColor.black().cgColor
mask.strokeColor = UIColor.yellow().cgColor
layer.addSublayer(sublayer)
layer.mask = mask
self.layer.addSublayer(layer)
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnim.toValue = NSValue.init(cgRect:finalRect)
let anim = CAAnimationGroup()
anim.animations = [boundsAnim]
anim.isRemovedOnCompletion = false
anim.duration = 3
anim.fillMode = kCAFillModeForwards
sublayer.add(anim, forKey: nil)
}
}
var badgeView:BadgeView = BadgeView(frame:CGRect.init(x: 50, y: 50, width: 50, height: 50))
var window:UIWindow = UIWindow(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
window.backgroundColor = UIColor.red()
badgeView.backgroundColor = UIColor.green()
window.becomeKey()
window.makeKeyAndVisible()
window.addSubview(badgeView)
badgeView.drawCircleInView()
More Modification to Above code , anchor point code was missing in above code
```
var str = "Hello, playground"
class BadgeView: UIView {
var coeff:CGFloat = 0.7
func drawCircleInView(){
// Set up the shape of the circle
let size:CGSize = self.bounds.size;
let layerBackGround = CALayer();
layerBackGround.frame = self.bounds;
layerBackGround.backgroundColor = UIColor.blue.cgColor
self.layer.addSublayer(layerBackGround)
let initialRect:CGRect = CGRect.init(x: 0, y: size.height , width: size.width, height: 0)
let finalRect:CGRect = CGRect.init(x: 0, y: 0, width: size.width, height: size.height)
let sublayer = CALayer()
//sublayer.bounds = initialRect
sublayer.frame = initialRect
sublayer.anchorPoint = CGPoint(x: 0.5, y: 1)
sublayer.backgroundColor = UIColor.orange.cgColor
sublayer.opacity = 1
let mask:CAShapeLayer = CAShapeLayer()
mask.frame = self.bounds
mask.path = UIBezierPath(ovalIn: self.bounds).cgPath
mask.fillColor = UIColor.black.cgColor
mask.strokeColor = UIColor.yellow.cgColor
layerBackGround.addSublayer(sublayer)
layerBackGround.mask = mask
self.layer.addSublayer(layerBackGround)
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnim.toValue = NSValue.init(cgRect:finalRect)
let anim = CAAnimationGroup()
anim.animations = [boundsAnim]
anim.isRemovedOnCompletion = false
anim.duration = 1
anim.fillMode = kCAFillModeForwards
sublayer.add(anim, forKey: nil)
}

Resources