I'm trying to animate color change of a bezier path in a UIView but I think I'm missing something very basic as the layer is not being rendered in my UI View. Just adding the layer to self.layer doesn't seem to trigger the draw in context method. I don't really understand the basics of which draw is being triggered when and into which layers.
public class ProgressIndicatorView: UIView {
var bubble = BubbleLayer()
public required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public class var sharedInstance: ProgressIndicatorView {
struct Singleton {
static let instance = ProgressIndicatorView(frame: CGRect.zeroRect)
}
return Singleton.instance
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.updateFrame(frame)
self.backgroundColor = UIColor(white: 0.0, alpha: 0.3)
self.layer.addSublayer(bubble)
}
func updateFrame(frame:CGRect)
{
self.frame = frame
bubble.frame = CGRect(x:self.center.x, y:self.center.y, width: 126, height: 126)
}
func animate(){
let anim = CABasicAnimation(keyPath:"percentage")
anim.fromValue = 0.000
anim.toValue = 1.000
anim.repeatCount = 1000
bubble.addAnimation(anim, forKey: "percentage")
}
public class func show() {
let window = UIApplication.sharedApplication().windows.first as UIWindow
let indicator = ProgressIndicatorView.sharedInstance
indicator.updateFrame(window.frame)
if indicator.superview == nil {
window.addSubview(indicator)
indicator.animate()
}
}
public class func hide(){
let indicator = ProgressIndicatorView.sharedInstance
UIView.animateWithDuration(0.33, delay: 0.0, options: .CurveEaseOut, animations: {
indicator.alpha = 0.0
}, completion: {_ in
indicator.alpha = 1.0
indicator.removeFromSuperview()
})
}
}
public class BubbleLayer : CALayer {
public var percentage:CGFloat = 0
public override func animationForKey(key: String!) -> CAAnimation! {
var anim = CABasicAnimation(keyPath: key)
anim.fromValue = self.presentationLayer().key
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
anim.duration = CATransaction.animationDuration()
return anim
}
public override func actionForKey(event: String!) -> CAAction! {
if (event == "percentage"){
return self.animationForKey(event)
}
else {
return super.actionForKey(event)
}
}
public override func drawInContext(ctx: CGContext!) {
super.drawInContext(ctx)
UIGraphicsPushContext(ctx)
let context = UIGraphicsGetCurrentContext()
let frame = self.bounds
let white = UIColor(red: self.percentage, green: 1.000, blue: 1.000, alpha: 1.000)
let white_20 = UIColor(red: self.percentage, green: 1.000, blue: 1.000, alpha: 0.200)
//// Oval 10 Drawing
CGContextSaveGState(context)
CGContextTranslateCTM(context, 1.3, 0.8)
var oval10Path = UIBezierPath(ovalInRect: CGRectMake(5, 5, 113.6, 113.6))
white_20.setStroke()
oval10Path.lineWidth = 10
oval10Path.stroke()
CGContextRestoreGState(context)
//// Bezier 122 Drawing
var bezier122Path = UIBezierPath()
bezier122Path.moveToPoint(CGPointMake(63, 5.9))
bezier122Path.addCurveToPoint(CGPointMake(119.8, 62.7), controlPoint1: CGPointMake(94.4, 5.9), controlPoint2: CGPointMake(119.8, 31.3))
bezier122Path.lineCapStyle = kCGLineCapRound;
bezier122Path.lineJoinStyle = kCGLineJoinRound;
white.setStroke()
bezier122Path.lineWidth = 10
bezier122Path.stroke()
}
}
Related
So, the problem is that the gradient stroke is applied in a way that is not clear to me. At the same time, the problem is only when I do the logic of clicking on the cells as a radio button. If you leave the default (multiple choice), then there is no problem. Maybe somewhere I'm missing changing the height of the view on reloading the collection. Also, if I remove the gradient and include just a stroke, then everything works well. Who has any ideas?
I add the gradient directly in the cell.
class InvestorTypeCollectionViewCell: UICollectionViewCell {
#IBOutlet private weak var cellTitleLabel: UILabel!
#IBOutlet private weak var cellDescriptionLabel: UILabel!
#IBOutlet private weak var checkMarkImageView: UIImageView!
#IBOutlet private weak var itemContainerView: UIView!
weak var delegate: InvestorTypeCollectionViewCellDelegate?
override func prepareForReuse() {
super.prepareForReuse()
checkMarkImageView.image = UIImage(named: "uncheckedBox")
// remove sublayer
itemContainerView.layer.sublayers?.filter{ $0 is CAGradientLayer }.forEach{ $0.removeFromSuperlayer() }
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var frame = layoutAttributes.frame
frame.size.height = ceil(size.height)
layoutAttributes.frame = frame
return layoutAttributes
}
func configCellWith(item: InvestorTypeModel, indexPath row: Int, selectedIndex: Int? = nil) {
itemContainerView.backgroundColor = UIColor.LIGHT_BLUE_BACKGROUND
itemContainerView.layer.cornerRadius = 8
cellTitleLabel.text = item.itemTitle.uppercased()
cellDescriptionLabel.text = Transformer.share.strippingOutHtmlFrom(text: item.itemDescription)
if item.checkBoxSelected {
diselectUISetUp()
// delegate to change item checkbox to false
delegate?.cellDidSelectedAt(indexPath: row, withCheckbox: false)
} else {
if let selectedIndex = selectedIndex {
if row == selectedIndex {
// select this cell
selectUISetUp()
// delegate to change item checkbox propertie to true
delegate?.cellDidSelectedAt(indexPath: row, withCheckbox: true)
}
}
}
}
private func diselectUISetUp() {
checkMarkImageView.image = UIImage(named: "uncheckedBox")
itemContainerView.layer.borderWidth = 0
itemContainerView.layer.borderColor = UIColor.clear.cgColor
itemContainerView.layer.cornerRadius = 8
}
private func selectUISetUp() {
checkMarkImageView.image = UIImage(named: "checkedBox")
// add gradient
let colors = [UIColor(red: 0.30, green: 0.84, blue: 0.74, alpha: 1.00),
UIColor(red: 0.44, green: 0.35, blue: 0.92, alpha: 1.00),
UIColor(red: 0.26, green: 0.20, blue: 0.87, alpha: 0.93)]
itemContainerView.gradientBorder(width: 1, colors: colors, andRoundCornersWithRadius: 8)
}
}
this is where the gradient is configured
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 0.5, y: 0.0),
endPoint: CGPoint = CGPoint(x: 0.5, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0) {
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
width: bounds.size.width + width, height: bounds.size.height + width)
border.colors = colors.map { return $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
mask.path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = width
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
When the data arrives, I change the size of the collection
func refreshData() {
DispatchQueue.main.async {
self.collectionView.reloadData()
guard let dataCount = self.viewModelPresenter?.data?.count else { return }
self.heightConstraintCV.constant = 1000 + 50
}
}
You will find it much easier to use a subclassed UIView that handles its own gradient border.
For example:
#IBDesignable
class GradientBorderView: UIView {
// turns on/off the gradient border
#IBInspectable public var selected: Bool = false { didSet { setNeedsLayout() } }
// default colors
// - can be set at run-time if desired
public var colors: [UIColor] = [UIColor(red: 0.30, green: 0.84, blue: 0.74, alpha: 1.00),
UIColor(red: 0.44, green: 0.35, blue: 0.92, alpha: 1.00),
UIColor(red: 0.26, green: 0.20, blue: 0.87, alpha: 0.93)]
{
didSet {
setNeedsLayout()
}
}
// default boder line width, corner radius, and inset-from-edges
// - can be set at run-time if desired
#IBInspectable public var bWidth: CGFloat = 1 { didSet { setNeedsLayout() } }
#IBInspectable public var cRadius: CGFloat = 8 { didSet { setNeedsLayout() } }
#IBInspectable public var inset: CGFloat = 0.5 { didSet { setNeedsLayout() } }
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
override func layoutSubviews() {
super.layoutSubviews()
let shapeLayer = CAShapeLayer()
if selected {
gLayer.colors = colors.compactMap( {$0.cgColor })
// make shapeLayer path the size of view inset by "inset"
// with rounded corners
// strokes are centered, so inset must be at least 1/2 of the border width
let mInset = max(inset, bWidth * 0.5)
shapeLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: mInset, dy: mInset), cornerRadius: cRadius).cgPath
// clear fill color
shapeLayer.fillColor = UIColor.clear.cgColor
// stroke color can be any non-clear color
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = bWidth
} else {
// we'll mask with an empty path
shapeLayer.path = UIBezierPath().cgPath
}
gLayer.mask = shapeLayer
}
}
So, use a GradientBorderView instead of a UIView:
#IBOutlet private weak var itemContainerView: GradientBorderView!
and all you have to do is set its selected property to show/hide the border:
itemContainerView.selected = true
or you could set .isHidden to show/hide it.
The "gradient border" will automatically update any time the view size changes.
The view is also marked #IBDesignable with #IBInspectable properties, so you can see how it looks while designing in Storyboard (note: selected is false by default, so you won't see anything unless you change that to true).
I'm trying to create a Shimmer effect to a UIButton, so i've subclassed it and tried to start the animation, but nothing happens. I can see the gradient on the button, but it is not animating. What am I doing wrong?
This is how it looks:
Here is my code:
class ShimmerButton: UIButton {
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
func addGradientLayer() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [0.0, 0.5, 1.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
return animation
}
func startAnimating(duration: Double, repeatCount: Float) {
let gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer.add(animation, forKey: "anim")
}
}
Starting the animation:
self.button.startAnimating(duration: 0.9, repeatCount: .infinity)
Code sample has been taken from here.
Animating the positions property works just fine.
I wrote a playground that creates a simple gradient animation. It looks like this:
I also incorporated the OPs ShimmerButton (With some changes to make it so you can start/stop the shimmer animation. The shimmer animation looks like this:
Here is the code for that playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ShimmerButton: UIButton {
public var animating = false
private var gradientLayer: CAGradientLayer?
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
private func addGradientLayer() -> CAGradientLayer? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [-1.0, -0.5, 0.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
public func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
animation.delegate = self
return animation
}
public func stopAnimating() {
guard let gradientLayer = gradientLayer else { return }
gradientLayer.removeFromSuperlayer()
gradientLayer.removeAllAnimations()
self.gradientLayer = nil
animating = false
}
public func startAnimating(duration: Double, repeatCount: Float) {
if !animating {
animating = true
gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer?.add(animation, forKey: "anim")
}
}
}
extension ShimmerButton: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished: Bool) {
animating = false
if finished {
gradientLayer?.removeFromSuperlayer()
gradientLayer = nil;
}
}
}
class GradientView: UIView {
var animationIsLeft: Bool = false
var animating = false
static override var layerClass: AnyClass {
return CAGradientLayer.self
}
public func animateGradient() {
animating = true
animateGradientStep()
}
public func animateGradientStep() {
guard animating,
let layer = layer as? CAGradientLayer else { return }
let animation = CABasicAnimation(keyPath:"locations")
animation.duration = 0.5
animationIsLeft = !animationIsLeft
animation.fromValue = animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
let toValue = !animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
animation.toValue = toValue
animation.delegate = self
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
layer.locations = toValue.map { NSNumber(value: $0) }
}
layer.add(animation, forKey: nil)
}
func doInitSetup() {
guard let layer = layer as? CAGradientLayer else { return }
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor, UIColor.blue.cgColor]
layer.locations = [0, 0.1, 1]
}
override init(frame: CGRect) {
super.init(frame: frame)
doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
doInitSetup()
}
}
extension GradientView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
animateGradientStep()
}
}
class MyViewController : UIViewController {
var gradientView: GradientView?
var button: ShimmerButton?
override func loadView() {
let view = UIView()
view.backgroundColor = .white
view.layer.borderWidth = 1
self.view = view
gradientView = GradientView()
if let gradientView = gradientView {
gradientView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradientView)
gradientView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
gradientView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
gradientView.heightAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.widthAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.layer.borderWidth = 1
}
button = ShimmerButton()
guard let button = button else { return }
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setTitle("Button", for: .normal)
button.backgroundColor = UIColor.lightGray
button.layer.borderWidth = 1
button.layer.cornerRadius = 10
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
button.widthAnchor.constraint(equalToConstant: 120).isActive = true
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#IBAction func buttonTapped(_ sender: UIButton) {
if gradientView?.animating == true {
gradientView?.animating = false
} else {
gradientView?.animateGradient()
}
guard let button = button else { return }
if button.animating == true {
button.stopAnimating()
} else {
button.startAnimating(duration: 1.0, repeatCount: Float.greatestFiniteMagnitude)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
PlaygroundPage.current.liveView = MyViewController()
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()
})
}
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
......
}
I use this class to create circular progress:
class ProgressBarView: UIView {
var bgPath: UIBezierPath!
var shapeLayer: CAShapeLayer!
var progressLayer: CAShapeLayer!
var progress: Float = 0 {
willSet(newValue)
{
progressLayer.strokeEnd = CGFloat(newValue)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
bgPath = UIBezierPath()
self.simpleShape()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
bgPath = UIBezierPath()
self.simpleShape()
}
func simpleShape()
{
createCirclePath()
shapeLayer = CAShapeLayer()
shapeLayer.path = bgPath.cgPath
shapeLayer.lineWidth = 2.5
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.black.cgColor.copy(alpha: 0.2)
progressLayer = CAShapeLayer()
progressLayer.path = bgPath.cgPath
progressLayer.lineCap = kCALineCapRound
progressLayer.lineWidth = 2.5
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1.0).cgColor
progressLayer.strokeEnd = 0.0
self.layer.addSublayer(shapeLayer)
self.layer.addSublayer(progressLayer)
}
private func createCirclePath()
{
let x = self.frame.width/2
let y = self.frame.height/2
let center = CGPoint(x: x, y: y)
print(x,y,center)
bgPath.addArc(withCenter: center, radius: x, startAngle: CGFloat(11), endAngle: CGFloat(17.28), clockwise: true)
bgPath.close()
}
}
And in my ViewController I use this class for my progressView. I want to change size of my progressView for iPhone SE. I trying create width constraint in storyboard and change size of progressView programmatically like:
if self.view.frame.height == 568 {
progressViewWidthConstraint.constant = 100
}
But it doesn't work. How to change size for iPhone SE?
Update
You need to re-layout
progressViewWidthConstraint.constant = 100
self.view.layoutIfNeeded()