iOS - Circle shaped gradient - ios

I am trying to draw a circular shaped gradient.
let backgroundView:UIView = UIView()
let backgroundLayer:CAShapeLayer = CAShapeLayer()
let gradient:CAGradientLayer = CAGradientLayer()
...
backgroundLayer.frame = CGRectMake(0, 0, backgroundDiameter, backgroundDiameter)
backgroundLayer.backgroundColor = UIColor.clearColor().CGColor
backgroundLayer.strokeColor = backgroundStrokeColor
backgroundLayer.fillColor = backgroundFillColor
gradient.colors = [UIColor(red: 0.5, green: 0.5, blue: 0.9, alpha: 1.0).CGColor,
UIColor(red: 0.9, green: 0.9, blue: 0.3, alpha: 1.0).CGColor]
gradient.locations = [0.01, 0.8]
gradient.frame = backgroundLayer.frame
backgroundView.frame = CGRectMake(0, 0, backgroundDiameter, backgroundDiameter)
backgroundView.backgroundColor = UIColor.clearColor()
backgroundView.center = ringControlCenter
backgroundLayer.insertSublayer(gradient, atIndex: 1)
backgroundLayer.path = CGPathCreateWithEllipseInRect(backgroundLayer.frame, nil)
backgroundView.layer.addSublayer(backgroundLayer)
self.addSubview(backgroundView)
However the gradient does not seem to be impacted by:
backgroundLayer.path = CGPathCreateWithEllipseInRect(backgroundLayer.frame, nil)
And still have its initial shape.
Is there a way to mask the gradient with an ellipse shaped layer without using CGContext* instructions?
Thank you,
MG

I've translated Leo library's in Swift 3.x , in other words , the good WCGradientCircleLayer writed in objective-C (it work exactly as aspected)
import UIKit
class WCGraintCircleLayer: CALayer {
override init () {
super.init()
}
convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) {
self.init()
self.bounds = bounds
self.position = position
let colors : [UIColor] = self.graint(fromColor: fromColor, toColor:toColor, count:4)
for i in 0..<colors.count-1 {
let graint = CAGradientLayer()
graint.bounds = CGRect(origin:CGPoint.zero, size: CGSize(width:bounds.width/2,height:bounds.height/2))
let valuePoint = self.positionArrayWith(bounds: self.bounds)[i]
graint.position = valuePoint
print("iesimo graint position: \(graint.position)")
let fromColor = colors[i]
let toColor = colors[i+1]
let colors : [CGColor] = [fromColor.cgColor,toColor.cgColor]
let stopOne: CGFloat = 0.0
let stopTwo: CGFloat = 1.0
let locations : [CGFloat] = [stopOne,stopTwo]
graint.colors = colors
graint.locations = locations as [NSNumber]? // with Swift 2 and Swift 3 you can cast directly a `CGFloat` value to `NSNumber` and back
graint.startPoint = self.startPoints()[i]
graint.endPoint = self.endPoints()[i]
self.addSublayer(graint)
//Set mask
let shapelayer = CAShapeLayer()
let rect = CGRect(origin:CGPoint.zero,size:CGSize(width:self.bounds.width - 2 * linewidth,height: self.bounds.height - 2 * linewidth))
shapelayer.bounds = rect
shapelayer.position = CGPoint(x:self.bounds.width/2,y: self.bounds.height/2)
shapelayer.strokeColor = UIColor.blue.cgColor
shapelayer.fillColor = UIColor.clear.cgColor
shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2).cgPath
shapelayer.lineWidth = linewidth
shapelayer.lineCap = kCALineCapRound
shapelayer.strokeStart = 0.010
let finalValue = (toValue*0.99)
shapelayer.strokeEnd = finalValue//0.99;
self.mask = shapelayer
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer {
let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue )
return layer
}
func graint(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
var result : [UIColor]! = [UIColor]()
for i in 0...count {
let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i)
let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i)
let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i)
let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i)
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
result.append(oneColor)
print(oneColor)
}
return result
}
func positionArrayWith(bounds:CGRect) -> [CGPoint]{
let first = CGPoint(x:(bounds.width/4)*3,y: (bounds.height/4)*1)
let second = CGPoint(x:(bounds.width/4)*3,y: (bounds.height/4)*3)
let third = CGPoint(x:(bounds.width/4)*1,y: (bounds.height/4)*3)
let fourth = CGPoint(x:(bounds.width/4)*1,y: (bounds.height/4)*1)
print([first,second,third,fourth])
return [first,second,third,fourth]
}
func startPoints() -> [CGPoint] {
return [CGPoint.zero,CGPoint(x:1,y:0),CGPoint(x:1,y:1),CGPoint(x:0,y:1)]
}
func endPoints() -> [CGPoint] {
return [CGPoint(x:1,y:1),CGPoint(x:0,y:1),CGPoint.zero,CGPoint(x:1,y:0)]
}
func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor {
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
let oneR = fromR + (toR - fromR) * progress
let oneG = fromG + (toG - fromG) * progress
let oneB = fromB + (toB - fromB) * progress
let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
return oneColor
}
// This is what you call if you want to draw a full circle.
func animateCircle(duration: TimeInterval) {
animateCircleTo(duration: duration, fromValue: 0.010, toValue: 0.99)
}
// This is what you call to draw a partial circle.
func animateCircleTo(duration: TimeInterval, fromValue: CGFloat, toValue: CGFloat){
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.isRemovedOnCompletion = true
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0.010 (no circle) to 0.99 (full circle)
animation.fromValue = 0.010
animation.toValue = toValue
// Do an easeout. Don't know how to do a spring instead
//animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
// Set the circleLayer's strokeEnd property to 0.99 now so that it's the
// right value when the animation ends.
let circleMask = self.mask as! CAShapeLayer
circleMask.strokeEnd = toValue
// Do the actual animation
circleMask.removeAllAnimations()
circleMask.add(animation, forKey: "animateCircle")
}
}
And this is a little example, how to use in Swift 3.x:
let gradientRingLayer = WCGraintCircleLayer(bounds: CGRect(origin: CGPoint.zero,size:CGSize(width: 150, height: 150)), position:CGPoint(x: 200, y: 300),fromColor:UIColor.blue, toColor:UIColor.white, linewidth:4.0, toValue:0)
self.view.layer.addSublayer(gradientRingLayer)
let duration = 3.0
gradientRingLayer.animateCircleTo(duration: duration, fromValue: 0, toValue: 0.99)
This is the code in Swift 2.x:
import UIKit
class WCGraintCircleLayer: CALayer {
override init () {
super.init()
}
convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) {
self.init()
self.bounds = bounds
self.position = position
let colors : [UIColor] = self.graintFromColor(fromColor, toColor:toColor, count:4)
for i in 0..<colors.count-1 {
let graint = CAGradientLayer()
graint.bounds = CGRectMake(0,0,CGRectGetWidth(bounds)/2,CGRectGetHeight(bounds)/2)
let valuePoint = self.positionArrayWithMainBounds(self.bounds)[i]
graint.position = valuePoint
print("iesimo graint position: \(graint.position)")
let fromColor = colors[i]
let toColor = colors[i+1]
let colors : [CGColorRef] = [fromColor.CGColor,toColor.CGColor]
let stopOne: CGFloat = 0.0
let stopTwo: CGFloat = 1.0
let locations : [CGFloat] = [stopOne,stopTwo]
graint.colors = colors
graint.locations = locations
graint.startPoint = self.startPoints()[i]
graint.endPoint = self.endPoints()[i]
self.addSublayer(graint)
//Set mask
let shapelayer = CAShapeLayer()
let rect = CGRectMake(0,0,CGRectGetWidth(self.bounds) - 2 * linewidth, CGRectGetHeight(self.bounds) - 2 * linewidth)
shapelayer.bounds = rect
shapelayer.position = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2)
shapelayer.strokeColor = UIColor.blueColor().CGColor
shapelayer.fillColor = UIColor.clearColor().CGColor
shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: CGRectGetWidth(rect)/2).CGPath
shapelayer.lineWidth = linewidth
shapelayer.lineCap = kCALineCapRound
shapelayer.strokeStart = 0.010
let finalValue = (toValue*0.99)
shapelayer.strokeEnd = finalValue//0.99;
self.mask = shapelayer
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer {
let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue )
return layer
}
func graintFromColor(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
var result : [UIColor]! = [UIColor]()
for i in 0...count {
let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i)
let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i)
let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i)
let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i)
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
result.append(oneColor)
print(oneColor)
}
return result
}
func positionArrayWithMainBounds(bounds:CGRect) -> [CGPoint]{
let first = CGPointMake((CGRectGetWidth(bounds)/4)*3, (CGRectGetHeight(bounds)/4)*1)
let second = CGPointMake((CGRectGetWidth(bounds)/4)*3, (CGRectGetHeight(bounds)/4)*3)
let third = CGPointMake((CGRectGetWidth(bounds)/4)*1, (CGRectGetHeight(bounds)/4)*3)
let fourth = CGPointMake((CGRectGetWidth(bounds)/4)*1, (CGRectGetHeight(bounds)/4)*1)
print([first,second,third,fourth])
return [first,second,third,fourth]
}
func startPoints() -> [CGPoint] {
return [CGPointMake(0,0),CGPointMake(1,0),CGPointMake(1,1),CGPointMake(0,1)]
}
func endPoints() -> [CGPoint] {
return [CGPointMake(1,1),CGPointMake(0,1),CGPointMake(0,0),CGPointMake(1,0)]
}
func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor {
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
let oneR = fromR + (toR - fromR) * progress
let oneG = fromG + (toG - fromG) * progress
let oneB = fromB + (toB - fromB) * progress
let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
return oneColor
}
// This is what you call if you want to draw a full circle.
func animateCircle(duration: NSTimeInterval) {
animateCircleTo(duration, fromValue: 0.010, toValue: 0.99)
}
// This is what you call to draw a partial circle.
func animateCircleTo(duration: NSTimeInterval, fromValue: CGFloat, toValue: CGFloat){
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.removedOnCompletion = true
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0.010 (no circle) to 0.99 (full circle)
animation.fromValue = 0.010
animation.toValue = toValue
// Do an easeout. Don't know how to do a spring instead
//animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
// Set the circleLayer's strokeEnd property to 0.99 now so that it's the
// right value when the animation ends.
let circleMask = self.mask as! CAShapeLayer
circleMask.strokeEnd = toValue
// Do the actual animation
circleMask.removeAllAnimations()
circleMask.addAnimation(animation, forKey: "animateCircle")
}
}
And this is a little example, how to use in Swift 2.x:
let gradientRingLayer = WCGraintCircleLayer(bounds: CGRectMake(0, 0, 150, 150), position:CGPointMake(200,300) ,fromColor:UIColor.blueColor(), toColor:UIColor.whiteColor(),linewidth:4.0, toValue:0)
self.view.layer.addSublayer(gradientRingLayer)
let duration = 3.0
gradientRingLayer.animateCircleTo(duration, fromValue: 0, toValue: 0.99)
This is rapid copy/paste remote pastebin code
Available also with animation:

I happen to visit this question,and want to post my answer.
The only thing,you need to do is use an CAShapeLayer to set Mask
[graintLayer setMask:shapeLayer]
I wrote an simple library about how to build Circle Graint Layer
This library is here
Here is the screenshot

If your backgroundLayer should just act as an mask for the gradient layer, use
gradient.mask = backgroundLayer
and add the gradient to the view's layer
backgroundView.layer.addSublayer(gradient)
otherwise create a new CAShapeLayer to act as maskLayer for the gradient

Here is Leo's library converted to Swift 3. Thanks to Alessandro Ornano's Swift 2.3 translation.
Code:
import UIKit
class WCGraintCircleLayer: CALayer {
override init () {
super.init()
}
convenience init(bounds:CGRect,position:CGPoint,fromColor:UIColor,toColor:UIColor,linewidth:CGFloat,toValue:CGFloat) {
self.init()
self.bounds = bounds
self.position = position
let colors : [UIColor] = self.graintFromColor(fromColor: fromColor, toColor:toColor, count:4)
for i in 0..<colors.count-1 {
let graint = CAGradientLayer()
graint.bounds = CGRect(x: 0, y: 0, width: bounds.width/2, height: bounds.height/2)
let valuePoint = self.positionArrayWithMainBounds(bounds: self.bounds)[i]
graint.position = valuePoint
print("iesimo graint position: \(graint.position)")
let fromColor = colors[i]
let toColor = colors[i+1]
let colors : [CGColor] = [fromColor.cgColor,toColor.cgColor]
let stopOne: CGFloat = 0.0
let stopTwo: CGFloat = 1.0
let locations : [CGFloat] = [stopOne,stopTwo]
graint.colors = colors
graint.locations = locations as [NSNumber]?
graint.startPoint = self.startPoints()[i]
graint.endPoint = self.endPoints()[i]
self.addSublayer(graint)
//Set mask
let shapelayer = CAShapeLayer()
let rect = CGRect(x: 0, y: 0, width: bounds.width - 2 * linewidth, height: bounds.height - 2 * linewidth)
shapelayer.bounds = rect
shapelayer.position = CGPoint(x: bounds.width/2, y: bounds.height/2)
shapelayer.strokeColor = UIColor.blue.cgColor
shapelayer.fillColor = UIColor.clear.cgColor
shapelayer.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2).cgPath
shapelayer.lineWidth = linewidth
shapelayer.lineCap = kCALineCapRound
shapelayer.strokeStart = 0.010
let finalValue = (toValue*0.99)
shapelayer.strokeEnd = finalValue//0.99;
self.mask = shapelayer
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layerWithWithBounds(bounds:CGRect, position:CGPoint, fromColor:UIColor, toColor:UIColor, linewidth : CGFloat,toValue:CGFloat) -> WCGraintCircleLayer {
let layer = WCGraintCircleLayer(bounds: bounds,position: position,fromColor:fromColor, toColor: toColor,linewidth: linewidth,toValue:toValue )
return layer
}
func graintFromColor(fromColor:UIColor, toColor:UIColor, count:Int) -> [UIColor]{
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
var result : [UIColor]! = [UIColor]()
for i in 0...count {
let oneR:CGFloat = fromR + (toR - fromR)/CGFloat(count) * CGFloat(i)
let oneG : CGFloat = fromG + (toG - fromG)/CGFloat(count) * CGFloat(i)
let oneB : CGFloat = fromB + (toB - fromB)/CGFloat(count) * CGFloat(i)
let oneAlpha : CGFloat = fromAlpha + (toAlpha - fromAlpha)/CGFloat(count) * CGFloat(i)
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
result.append(oneColor)
print(oneColor)
}
return result
}
func positionArrayWithMainBounds(bounds:CGRect) -> [CGPoint]{
let first = CGPoint(x: (bounds.width/4)*3, y: (bounds.height/4)*1)
let second = CGPoint(x: (bounds.width/4)*3, y: (bounds.height/4)*3)
let third = CGPoint(x: (bounds.width/4)*1, y: (bounds.height/4)*3)
let fourth = CGPoint(x: (bounds.width/4)*1, y: (bounds.height/4)*1)
print([first,second,third,fourth])
return [first,second,third,fourth]
}
func startPoints() -> [CGPoint] {
return [CGPoint(x: 0, y: 0),CGPoint(x: 1, y: 0),CGPoint(x: 1, y: 1),CGPoint(x: 0, y: 1)]
}
func endPoints() -> [CGPoint] {
return [CGPoint(x: 1, y: 1),CGPoint(x: 0, y: 1),CGPoint(x: 0, y: 0),CGPoint(x: 1, y: 0)]
}
func midColorWithFromColor(fromColor:UIColor, toColor:UIColor, progress:CGFloat) -> UIColor {
var fromR:CGFloat = 0.0,fromG:CGFloat = 0.0,fromB:CGFloat = 0.0,fromAlpha:CGFloat = 0.0
fromColor.getRed(&fromR,green: &fromG,blue: &fromB,alpha: &fromAlpha)
var toR:CGFloat = 0.0,toG:CGFloat = 0.0,toB:CGFloat = 0.0,toAlpha:CGFloat = 0.0
toColor.getRed(&toR,green: &toG,blue: &toB,alpha: &toAlpha)
let oneR = fromR + (toR - fromR) * progress
let oneG = fromG + (toG - fromG) * progress
let oneB = fromB + (toB - fromB) * progress
let oneAlpha = fromAlpha + (toAlpha - fromAlpha) * progress
let oneColor = UIColor.init(red: oneR, green: oneG, blue: oneB, alpha: oneAlpha)
return oneColor
}
// This is what you call if you want to draw a full circle.
func animateCircle(duration: TimeInterval) {
animateCircleTo(duration: duration, fromValue: 0.010, toValue: 0.99)
}
// This is what you call to draw a partial circle.
func animateCircleTo(duration: TimeInterval, fromValue: CGFloat, toValue: CGFloat){
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.isRemovedOnCompletion = true
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0.010 (no circle) to 0.99 (full circle)
animation.fromValue = 0.010
animation.toValue = toValue
// Do an easeout. Don't know how to do a spring instead
//animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
// Set the circleLayer's strokeEnd property to 0.99 now so that it's the
// right value when the animation ends.
let circleMask = self.mask as! CAShapeLayer
circleMask.strokeEnd = toValue
// Do the actual animation
circleMask.removeAllAnimations()
circleMask.add(animation, forKey: "animateCircle")
}
}
How to use:
let gradientRingLayer = WCGraintCircleLayer(bounds: CGRect(x: 0, y: 0, width: 150, height: 150), position:CGPoint(x: 200, y: 300),fromColor:UIColor.blue, toColor:UIColor.white, linewidth:4.0, toValue:0)
self.view.layer.addSublayer(gradientRingLayer)
let duration = 3.0
gradientRingLayer.animateCircleTo(duration: duration, fromValue: 0, toValue: 0.99)

I found a simpler answer without using mulitple layers, blending multiple layers may impact animation performance, generally should be avoided as much as possible.
override func draw(_ rect: CGRect) {
let lineWidth: CGFloat = CGFloat(2)
let strokeColor: UIColor = UIColor.black
let startAngle: CGFloat = 0
let maxAngle: CGFloat = CGFloat(Double.pi * 2)
let lineCapStyle: CGLineCap = .round
let gradations = 255
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
let radius = (min(bounds.size.width, bounds.size.height) - lineWidth) / 2
for i in 1 ... gradations {
let percent0 = CGFloat(i - 1) / CGFloat(gradations)
let percent1 = CGFloat(i) / CGFloat(gradations)
let angle0 = startAngle + (maxAngle - startAngle) * percent0
let angle1 = startAngle + (maxAngle - startAngle) * percent1
let context = UIGraphicsGetCurrentContext()!
context.setLineWidth(lineWidth)
context.setLineCap(lineCapStyle)
let path = CGMutablePath()
path.addArc(center: center, radius: radius + lineWidth / 2, startAngle: angle0, endAngle: angle1, clockwise: true)
path.addArc(center: center, radius: radius - lineWidth / 2, startAngle: angle1, endAngle: angle0, clockwise: false)
path.closeSubpath()
let colors = [strokeColor.withAlphaComponent(percent0).cgColor, strokeColor.withAlphaComponent(percent1).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
let startPoint = CGPoint.init(x: center.x + cos(angle0) * radius, y: center.y + sin(angle0) * radius)
let endPoint = CGPoint.init(x: center.x + cos(angle1) * radius, y: center.y + sin(angle1) * radius)
context.saveGState()
context.addPath(path)
context.clip()
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
context.restoreGState()
}
}

As of iOS 12, you can use kCAGradientLayerConic as the type for your gradient layer:
// Conical gradient layer
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.clockView.bounds;
gradientLayer.type = kCAGradientLayerConic;
gradientLayer.startPoint = CGPointMake(0.5, 0.5);
gradientLayer.endPoint = CGPointMake(1, 1.0);
gradientLayer.colors = #[(__bridge id)[UIColor colorWithRed:1.0/255.0 green:25.0/255.0 blue:147.0/255.0 alpha:1.0].CGColor,
(__bridge id)[UIColor colorWithRed:250.0/255.0 green:226.0/255.0 blue:50.0/255.0 alpha:1.0].CGColor,
(__bridge id)[UIColor colorWithRed:250.0/255.0 green:226.0/255.0 blue:50.0/255.0 alpha:1.0].CGColor,
(__bridge id)[UIColor colorWithRed:1.0/255.0 green:25.0/255.0 blue:147.0/255.0 alpha:1.0].CGColor];
gradientLayer.locations = #[[NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:0.4], [NSNumber numberWithFloat:0.5], [NSNumber numberWithFloat:0.6], [NSNumber numberWithFloat:1.0f]];
// Circular mask for gradient layer
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = gradientLayer.bounds;
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, CGRectInset(self.view.layer.bounds, 10.0, 10.0));
maskLayer.path = circlePath;
gradientLayer.mask = maskLayer;
[self.view.layer addSublayer:gradientLayer];
CGPathRelease(circlePath);
// Place a second layer over the gradient layer...
CALayer *circleLayer = [CALayer layer];
circleLayer.frame = gradientLayer.bounds;
[circleLayer setBackgroundColor:[UIColor darkGrayColor].CGColor];
// ...and mask it with a smaller circle
CAShapeLayer *maskLayer2 = [CAShapeLayer layer];
maskLayer2.frame = circleLayer.bounds;
CGMutablePathRef circlePath2 = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath2, NULL, CGRectInset(self.view.layer.bounds, 60.0, 60.0));
maskLayer2.path = circlePath2;
CGPathRelease(circlePath2);
circleLayer.mask = maskLayer2;
[self.view.layer insertSublayer:circleLayer atIndex:1];

Related

How do I get the coordinates from CAShapeLayer

So I am trying to make a progress bar. So I have made circular path, but I want the dot to be at the end of the progress bar, but how do I get the position of the dot to be att the end of the current progress?
private func simpleShape() {
let width: CGFloat = 10
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
backLayer.strokeEnd = 1
self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
circle.layoutIfNeeded()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
progressLayer.strokeEnd = CGFloat(progress)
if let progressEndpoint = progressLayer.path?.currentPoint {
dotLayer.position = progressEndpoint
}
}
This is what I'm getting
This is what I want
You’re going to have to calculate it yourself. So figure out the angle from the start and end angles for your arcs:
let angle = (endAngle - startAngle) * progress + startAngle
And then use basic trigonometry to determine where that point falls:
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
By the way, I’d suggest separating the adding of the sublayers (which is part of the initial configuration process) from the updating paths (which is part of the view layout process, which may be called again if the frame of the view changes, constraints are applied, etc). Thus, perhaps:
#IBDesignable
class ProgressView: UIView {
var progress: CGFloat = 0 { didSet { updateProgress() } }
private var centerPoint: CGPoint = .zero
private var radius: CGFloat = 0
private let startAngle: CGFloat = -0.475 * .pi
private let endAngle: CGFloat = 1.525 * .pi
private let lineWidth: CGFloat = 10
private lazy var progressLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = progress
return shapeLayer
}()
private lazy var backLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
return shapeLayer
}()
private lazy var dotLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(arcCenter: .zero, radius: lineWidth / 2 * 1.75, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
shapeLayer.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSublayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
updateProgress()
}
}
private extension ProgressView {
func addSublayers() {
layer.addSublayer(backLayer)
layer.addSublayer(progressLayer)
layer.addSublayer(dotLayer)
}
func updatePaths() {
centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
radius = min(bounds.width, bounds.height) / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
}
func updateProgress() {
progressLayer.strokeEnd = progress
let angle = (endAngle - startAngle) * progress + startAngle
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
}
}
What you need is rotation Animation
let progressLayer = CAShapeLayer()
let backLayer = CAShapeLayer()
private func simpleShape() {
let width: CGFloat = 15
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = #colorLiteral(red: 0.888897419, green: 0.5411034822, blue: 0.04008810222, alpha: 1)
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1)
backLayer.strokeEnd = 1
// self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let distance = circle.bounds.width / 2 * 0.17
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
let circleCenter = CGPoint(x:centerPoint.x - distance,y:centerPoint.y - circleRadius )
let dotCircle = UIBezierPath()
dotCircle.addArc(withCenter:circleCenter, radius: 3, startAngle: CGFloat(-90).deg2rad(), endAngle: CGFloat(270).deg2rad(), clockwise: true)
dotLayer.path = dotCircle.cgPath
dotLayer.position = CGPoint(x:centerPoint.x,y:centerPoint.y )
dotLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.6496753961)
dotLayer.lineWidth = 10
dotLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
dotLayer.isHidden = true
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
print(progress)
// progressLayer.strokeEnd = progress
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.5;
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
animation.autoreverses = false
animation.repeatCount = .nan
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "line")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dotLayer.isHidden = false
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
// rotateAnimation.beginTime = CACurrentMediaTime() + 0.5;
rotateAnimation.fromValue = (CGFloat( -90)).deg2rad()
rotateAnimation.toValue = (360*progress - 98).deg2rad()
rotateAnimation.duration = 2
rotateAnimation.fillMode = .forwards
rotateAnimation.isRemovedOnCompletion = false
self.dotLayer.add(rotateAnimation, forKey: nil)
}
}

Display an array of CAShapeLayers

I have created an array of CAShapeLayers in order to draw different portions of an arc in different colors on different layers, this is my code :
import UIKit
class ViewController: UIViewController {
var level = 0.0
var old_level = 0.75
var progressLayer: [CAShapeLayer] = []
var circle = UIView()
override func viewDidLoad() {
circle = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
view.addSubview(circle)
update_curve(value: 10)
update_curve(value: 20)
update_curve(value: 10)
}
func update_curve(value: Double){
level = value*(1.5/100)+0.75
progressLayer.append(CAShapeLayer())
var progressPath = UIBezierPath(arcCenter: CGPoint(x: progressLayer[layerindex].frame.size.width/2, y: progressLayer[layerindex].frame.size.height/2), radius: CGFloat(100), startAngle: CGFloat(M_PI*old_level), endAngle:CGFloat(level*M_PI), clockwise: true)
circle.layer.addSublayer(progressLayer[layerindex])
progressLayer[layerindex].frame = view.bounds
progressLayer[layerindex].path = progressPath.cgPath
progressLayer[layerindex].fillColor = UIColor.clear.cgColor
progressLayer[layerindex].strokeColor = generateRandomColor()
progressLayer[layerindex].lineWidth = 20.0
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 1
animation2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
progressLayer[layerindex].add(animation2, forKey: "drawLineAnimation")
layerindex += 1
old_level = level
}
func generateRandomColor() -> UIColor {
let hue : CGFloat = CGFloat(arc4random() % 256) / 256 // use 256 to get full range from 0.0 to 1.0
let saturation : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from white
let brightness : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from black
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1)
}
}
But when I run this code, nothing appears on the screen of the iphone, but I don't get an error. What am I doing wrong?
I have just realised that the frame of the progressLayer was being defined after the defining the progressPath. The code for the update_curve function should be :
func update_curve(value: Double){
level = value*(1.5/100)+0.75
progressLayer.append(CAShapeLayer())
progressLayer[layerindex].frame = view.bounds
var progressPath = UIBezierPath(arcCenter: CGPoint(x: progressLayer[layerindex].frame.size.width/2, y: progressLayer[layerindex].frame.size.height/2), radius: CGFloat(100), startAngle: CGFloat(M_PI*old_level), endAngle:CGFloat(level*M_PI), clockwise: true)
circle.layer.addSublayer(progressLayer[layerindex])
progressLayer[layerindex].path = progressPath.cgPath
progressLayer[layerindex].fillColor = UIColor.clear.cgColor
progressLayer[layerindex].strokeColor = generateRandomColor()
progressLayer[layerindex].lineWidth = 20.0
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 1
animation2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
progressLayer[layerindex].add(animation2, forKey: "drawLineAnimation")
layerindex += 1
old_level = level
}

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()
}
}

Create spinning circle loading animation

I'm trying to create a spinning circle loader animation like in the following Android project (https://github.com/pedant/sweet-alert-dialog).
I don't need the entire popup dialog - just the spinning part. It changes colors and spins indefinitly (until I choose to dismiss it).
I'm kind of new to swift and I've never been the kind to do animations. Here's what I have so far (found code in similar project for iOS):
The layers setup:
outlineLayer.position = CGPointMake(0,
0);
outlineLayer.path = outlineCircle
outlineLayer.fillColor = UIColor.clearColor().CGColor;
outlineLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
outlineLayer.lineCap = kCALineCapRound
outlineLayer.lineWidth = 4;
outlineLayer.opacity = 0.1
self.layer.addSublayer(outlineLayer)
circleLayer.position = CGPointMake(0,
0);
circleLayer.path = path
circleLayer.fillColor = UIColor.clearColor().CGColor;
circleLayer.strokeColor = UIColor(red: 150.0/255.0, green: 216.0/255.0, blue: 115.0/255.0, alpha: 1.0).CGColor;
circleLayer.lineCap = kCALineCapRound
circleLayer.lineWidth = 4;
circleLayer.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(circleLayer)
Animation:
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
let factor = 0.545
let timing = CAMediaTimingFunction(controlPoints: 0.3, 0.6, 0.8, 1.2)
strokeEnd.fromValue = 0.00
strokeEnd.toValue = 0.93
strokeEnd.duration = 10.0 * factor
strokeEnd.timingFunction = timing
strokeEnd.autoreverses = true
strokeStart.fromValue = 0.0
strokeStart.toValue = 0.68
strokeStart.duration = 10.0 * factor
strokeStart.beginTime = CACurrentMediaTime() + 3.0 * factor
strokeStart.fillMode = kCAFillModeBackwards
strokeStart.timingFunction = timing
strokeStart.repeatCount = HUGE
circleLayer.strokeStart = 0.68
circleLayer.strokeEnd = 0.93
self.circleLayer.addAnimation(strokeEnd, forKey: "strokeEnd")
self.circleLayer.addAnimation(strokeStart, forKey: "strokeStart")
but what I have is not nearly close and I have no idea where to go from here. What I'm doing is changing a value and running seeing how it affects but I feel like I'm lost here.
How can I achieve such animation like in the example?
I didn't closely analyze the exact parameters of the animation, but this looks good to me:
import UIKit
#IBDesignable
class SpinnerView : UIView {
override var layer: CAShapeLayer {
get {
return super.layer as! CAShapeLayer
}
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
layer.fillColor = nil
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
setPath()
}
override func didMoveToWindow() {
animate()
}
private func setPath() {
layer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: layer.lineWidth / 2, dy: layer.lineWidth / 2)).cgPath
}
struct Pose {
let secondsSincePriorPose: CFTimeInterval
let start: CGFloat
let length: CGFloat
init(_ secondsSincePriorPose: CFTimeInterval, _ start: CGFloat, _ length: CGFloat) {
self.secondsSincePriorPose = secondsSincePriorPose
self.start = start
self.length = length
}
}
class var poses: [Pose] {
get {
return [
Pose(0.0, 0.000, 0.7),
Pose(0.6, 0.500, 0.5),
Pose(0.6, 1.000, 0.3),
Pose(0.6, 1.500, 0.1),
Pose(0.2, 1.875, 0.1),
Pose(0.2, 2.250, 0.3),
Pose(0.2, 2.625, 0.5),
Pose(0.2, 3.000, 0.7),
]
}
}
func animate() {
var time: CFTimeInterval = 0
var times = [CFTimeInterval]()
var start: CGFloat = 0
var rotations = [CGFloat]()
var strokeEnds = [CGFloat]()
let poses = type(of: self).poses
let totalSeconds = poses.reduce(0) { $0 + $1.secondsSincePriorPose }
for pose in poses {
time += pose.secondsSincePriorPose
times.append(time / totalSeconds)
start = pose.start
rotations.append(start * 2 * .pi)
strokeEnds.append(pose.length)
}
times.append(times.last!)
rotations.append(rotations[0])
strokeEnds.append(strokeEnds[0])
animateKeyPath(keyPath: "strokeEnd", duration: totalSeconds, times: times, values: strokeEnds)
animateKeyPath(keyPath: "transform.rotation", duration: totalSeconds, times: times, values: rotations)
animateStrokeHueWithDuration(duration: totalSeconds * 5)
}
func animateKeyPath(keyPath: String, duration: CFTimeInterval, times: [CFTimeInterval], values: [CGFloat]) {
let animation = CAKeyframeAnimation(keyPath: keyPath)
animation.keyTimes = times as [NSNumber]?
animation.values = values
animation.calculationMode = .linear
animation.duration = duration
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
func animateStrokeHueWithDuration(duration: CFTimeInterval) {
let count = 36
let animation = CAKeyframeAnimation(keyPath: "strokeColor")
animation.keyTimes = (0 ... count).map { NSNumber(value: CFTimeInterval($0) / CFTimeInterval(count)) }
animation.values = (0 ... count).map {
UIColor(hue: CGFloat($0) / CGFloat(count), saturation: 1, brightness: 1, alpha: 1).cgColor
}
animation.duration = duration
animation.calculationMode = .linear
animation.repeatCount = Float.infinity
layer.add(animation, forKey: animation.keyPath)
}
}
Try out my three custom Loader screen very simple :
Write below code in Viewcontoller.swift file
class ViewController: UIViewController {
var signView = SignView(frame: CGRect.zero)
var testView = TestView(frame: CGRect.zero)
var testView1 = TestView1(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.orange
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addSignView()
//addTestView()
//addTestView1()
}
func addSignView() {
signView.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(signView)
signView.addAnimationLayer()
}
func addTestView() {
let boxSize: CGFloat = 200.0
testView.frame = CGRect(x: 16,
y: 350,
width: boxSize,
height: boxSize)
self.view.addSubview(testView)
testView.addAnimationLayer()
}
func addTestView1() {
testView1.frame = CGRect(x: 0,
y: 0,
width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
self.view.addSubview(testView1)
testView1.addAnimationLayer()
}}
Now Add 3 Files inherit with UiView named > SignView , TestView and TestView1
Code for SignView.swift file
class SignView: UIView {
let upCircleLayer = CAShapeLayer.init()
var path = UIBezierPath.init()
var animationDuration : Double = 2
var frameHeight : CGFloat = 50.0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var signWavePath : UIBezierPath {
var clockCycle = true
let yPoint = self.frame.size.height/2
frameHeight = self.frame.size.width/6
for x in 1...24{
if x%2 != 0 {
let xpath = UIBezierPath(arcCenter: CGPoint(x: CGFloat(x)*frameHeight/2, y: yPoint),
radius: frameHeight/2,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: clockCycle)
path.append(xpath)
if(clockCycle){
clockCycle = false
}
else{
clockCycle = true
}
}
}
return path;
}
func addAnimationLayer() {
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.white.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = signWavePath.cgPath
layer.addSublayer(upCircleLayer)
animateStrokeUpCircle()
Timer.scheduledTimer(timeInterval: animationDuration, target: self, selector: #selector(animateStrokeUpCircle), userInfo: nil, repeats: true)
}
func animateStrokeUpCircle() {
let strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0.0
strokeAnimation.toValue = 1.0
strokeAnimation.duration = animationDuration
strokeAnimation.isRemovedOnCompletion = false
upCircleLayer.add(strokeAnimation, forKey: nil)
expand1()
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,sin(self.frame.width)]
expandAnimation.toValue = [-self.frame.width,cos(self.frame.width)]
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
Code for TestView File :
class TestView: UIView {
let upCircleLayer = CAShapeLayer.init()
let downCircleLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var up1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up22Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
}
var down11Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
}
var up2Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: 3*self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: true)
}
var down1Circle: UIBezierPath {
return UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/4, y: self.frame.size.height/2),
radius: self.frame.size.height/4,
startAngle: 0.0,
endAngle: 180.0 * .pi/180.0,
clockwise: false)
}
func addAnimationLayer() {
path1.append(up1Circle);
path1.append(down2Circle);
path2.append(down11Circle)
path2.append(up22Circle)
// Add Upper Circle Layer
upCircleLayer.fillColor = UIColor.clear.cgColor
upCircleLayer.strokeColor = UIColor.black.cgColor
upCircleLayer.lineWidth = 8.0
upCircleLayer.path = path1.cgPath
layer.addSublayer(upCircleLayer)
Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand() {
if animationDirection{
//upCircleLayer.path = path1.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
//upCircleLayer.path = path2.cgPath
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = 1.5
//expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,self.frame.height/2]
expandAnimation.toValue = 500
expandAnimation.duration = 2.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
upCircleLayer.add(expandAnimation, forKey: nil)
}
}
And Code for TestView1.swift file
class TestView1: UIView {
let animationLayer = CAShapeLayer.init()
var path1 = UIBezierPath.init()
var path2 = UIBezierPath.init()
var path = UIBezierPath.init()
var circleRadius : CGFloat = 26.0;
var centerLineHeight : CGFloat = 40.0
var animationDuration : Double = 2.0
var animationDirection : Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.black
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var centerMainLine: UIBezierPath {
let frameSize = self.frame.size
let centerLine = UIBezierPath()
centerLine.move(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 - centerLineHeight/2))
centerLine.addLine(to: CGPoint(x: frameSize.width/2, y: frameSize.height/2 + centerLineHeight/2))
return centerLine
}
var upLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var upRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 - centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
var downLeftCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 - circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
var downRightCircle: UIBezierPath {
let frameSize = self.frame.size
let halfCircle = UIBezierPath(arcCenter: CGPoint(x: frameSize.width/2 + circleRadius, y: frameSize.height/2 + centerLineHeight/2),
radius: circleRadius,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawUpCircle(centerPoint:CGPoint, radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: true)
return halfCircle
}
func drawDownCircle(centerPoint:CGPoint,radiusValue:CGFloat) -> UIBezierPath {
let halfCircle = UIBezierPath(arcCenter: centerPoint,
radius: radiusValue,
startAngle: 180.0 * .pi/180.0,
endAngle: 0.0,
clockwise: false)
return halfCircle
}
func drawLine(fromPoint:CGPoint,toPoint:CGPoint) -> UIBezierPath {
let line = UIBezierPath()
line.move(to: fromPoint)
line.addLine(to: toPoint)
return line
}
func addAnimationLayer() {
createPathOne()
createPathTwo()
createPath()
// set Animation Layer design
animationLayer.fillColor = UIColor.clear.cgColor
animationLayer.strokeColor = UIColor.white.cgColor
animationLayer.lineWidth = 8.0
animationLayer.path = path.cgPath
layer.addSublayer(animationLayer)
expand1()
Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(expand1), userInfo: nil, repeats: true)
}
func expand1() {
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "position")
expandAnimation.fromValue = [0,0]
expandAnimation.toValue = [-2000,0]
expandAnimation.duration = 10.0
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
}
func expand() {
animationLayer.path = centerMainLine.cgPath
if animationDirection{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path1.cgPath
expandAnimation.toValue = path2.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeBackwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = false
}
else{
let expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expandAnimation.fromValue = path2.cgPath
expandAnimation.toValue = path1.cgPath
expandAnimation.duration = animationDuration
expandAnimation.fillMode = kCAFillModeForwards
expandAnimation.isRemovedOnCompletion = false
animationLayer.add(expandAnimation, forKey: nil)
animationDirection = true
}
}
func createPathOne(){
path1.append(upLeftCircle);
path1.append(centerMainLine);
path1.append(downRightCircle)
}
func createPathTwo(){
path2.append(downLeftCircle);
path2.append(centerMainLine);
path2.append(upRightCircle)
}
func createPath() {
let frameSize = self.frame.size;
let lineHeight1 : CGFloat = 30
let lineHeight2 : CGFloat = 20
let radius1 : CGFloat = 40.0
let radius2 : CGFloat = 20.0
var lastPoint : CGPoint = CGPoint(x:0.0,y:frameSize.height/2 - lineHeight1/2)
for i in 1...10{
let p1 = drawUpCircle(centerPoint: CGPoint(x: lastPoint.x + radius1, y: lastPoint.y ), radiusValue: radius1)
lastPoint = p1.currentPoint;
let p2 = drawLine(fromPoint: lastPoint , toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y+lineHeight1))
lastPoint = p2.currentPoint;
let p3 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius1, y: lastPoint.y), radiusValue: radius1)
lastPoint = p3.currentPoint;
let p4 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight2))
lastPoint = p4.currentPoint;
let p5 = drawUpCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p5.currentPoint;
let p6 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y + lineHeight2))
lastPoint = p6.currentPoint;
let p7 = drawDownCircle(centerPoint: CGPoint(x:lastPoint.x + radius2, y: lastPoint.y), radiusValue: radius2)
lastPoint = p7.currentPoint
let p8 = drawLine(fromPoint: lastPoint, toPoint: CGPoint(x:lastPoint.x, y: lastPoint.y - lineHeight1))
lastPoint = p8.currentPoint;
path.append(p1)
path.append(p2)
path.append(p3)
path.append(p4)
path.append(p5)
path.append(p6)
path.append(p7)
path.append(p8)
}
}
}
Now run the code to check the Animation Loader. Comment/Uncomment other 2 loader method in viewDidAppear method of viewcontroller.
Enjoy!!
If someone is looking for Objective C version of #rob mayoff solution
in SpinnerView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface SpinnerView : UIView
#end
in SpinnerView.m
#import "SpinnerView.h"
#import "Pose.h"
#implementation SpinnerView
- (instancetype) initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
return self;
}
- (instancetype) initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
return self;
}
- (CAShapeLayer*) layer {
return (CAShapeLayer*)super.layer;
}
- (CAShapeLayer*) getLayer{
return (CAShapeLayer*)super.layer;
}
+ (Class)layerClass{
return [CAShapeLayer class];
}
- (void) layoutSubviews{
[super layoutSubviews];
[self getLayer].fillColor = nil;
[self getLayer].strokeColor = [UIColor blackColor].CGColor;
[self getLayer].lineWidth = 3;
[self setPath];
}
- (void) didMoveToWindow{
[self animate];
}
- (void) setPath{
UIBezierPath* bezierPath = ([UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, [self getLayer].lineWidth/2, [self getLayer].lineWidth/2)]);
[self getLayer].path = bezierPath.CGPath;
}
- (NSArray*) poses{
NSMutableArray* poses = [[NSMutableArray alloc] init];
[poses addObject:[[Pose alloc] initWith:0.0 start:0.000 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.6 start:0.500 length:0.5]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.000 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.6 start:1.500 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:1.875 length:0.1]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.250 length:0.3]];
[poses addObject:[[Pose alloc] initWith:0.2 start:2.625 length:0.7]];
[poses addObject:[[Pose alloc] initWith:0.2 start:3.000 length:0.5]];
return poses;
}
- (void) animate{
CFTimeInterval time = 0;
NSMutableArray* times = [NSMutableArray new];;
CGFloat start = 0;
NSMutableArray* rotations = [NSMutableArray new];
NSMutableArray* strokeEnds = [NSMutableArray new];
NSArray* posses = [self poses];
double totalSeconds = [[posses valueForKeyPath:#"#sum.secondsSincePriorPose"] doubleValue];
for(Pose* pose in posses){
time += pose.secondsSincePriorPose;
[times addObject:[NSNumber numberWithDouble:time/totalSeconds]];
start = pose.start;
[rotations addObject:[NSNumber numberWithDouble:start*2*M_PI]];
[strokeEnds addObject:[NSNumber numberWithDouble:pose.length]];
}
[times addObject:[times lastObject]];
[rotations addObject:[rotations firstObject]];
[strokeEnds addObject:[strokeEnds firstObject]];
[self animateKeyPath:#"strokeEnd" duration:totalSeconds times:times values:strokeEnds];
[self animateKeyPath:#"transform.rotation" duration:totalSeconds times:times values:rotations];
[self animateStrokeHueWithDuration:totalSeconds * 5];
}
- (void) animateKeyPath:(NSString*)keyPath duration:(CFTimeInterval)duration times:(NSArray*)times values:(NSArray*)values{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:keyPath];
animation.keyTimes = times;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
- (void) animateStrokeHueWithDuration:(CFTimeInterval)duration{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"strokeColor"];
NSMutableArray *keyTimes = [NSMutableArray array];
NSMutableArray *values = [NSMutableArray array];
for (NSInteger i = 0; i < 36; i++) {
[keyTimes addObject: [NSNumber numberWithDouble:(CFTimeInterval)i/(CFTimeInterval)36]];
[values addObject:(id)[UIColor colorWithHue:(CGFloat)i/(CGFloat)36 saturation:1 brightness:1 alpha:1].CGColor];
}
animation.keyTimes = keyTimes;
animation.values = values;
animation.calculationMode = kCAAnimationLinear;
animation.duration = duration;
animation.repeatCount = FLT_MAX;
[[self getLayer] addAnimation:animation forKey:animation.keyPath];
}
#end
Pose.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface Pose : NSObject
#property CFTimeInterval secondsSincePriorPose;
#property CGFloat start;
#property CGFloat length;
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length;
#end
Pose.m
#import "Pose.h"
#import <UIKit/UIKit.h>
#implementation Pose
- (instancetype) initWith:(CFTimeInterval)timeInterval start:(CGFloat)start length:(CGFloat)length{
self = [super init];
self.start = start;
self.length = length;
self.secondsSincePriorPose = timeInterval;
return self;
}
#end

Dashed line border around UIView

How do I add dashed line border around UIView.
Something Like this
Another method if you like sublayers. In your custom view's init, put this (_border is an ivar):
_border = [CAShapeLayer layer];
_border.strokeColor = [UIColor colorWithRed:67/255.0f green:37/255.0f blue:83/255.0f alpha:1].CGColor;
_border.fillColor = nil;
_border.lineDashPattern = #[#4, #2];
[self.layer addSublayer:_border];
And in your layoutsubviews, put this:
_border.path = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
_border.frame = self.bounds;
You can set the border with this pattern using Layer and Bezier path like below examples.
Objective-C
CAShapeLayer *yourViewBorder = [CAShapeLayer layer];
yourViewBorder.strokeColor = [UIColor blackColor].CGColor;
yourViewBorder.fillColor = nil;
yourViewBorder.lineDashPattern = #[#2, #2];
yourViewBorder.frame = yourView.bounds;
yourViewBorder.path = [UIBezierPath bezierPathWithRect:yourView.bounds].CGPath;
[yourView.layer addSublayer:yourViewBorder];
Swift 3.1
var yourViewBorder = CAShapeLayer()
yourViewBorder.strokeColor = UIColor.black.cgColor
yourViewBorder.lineDashPattern = [2, 2]
yourViewBorder.frame = yourView.bounds
yourViewBorder.fillColor = nil
yourViewBorder.path = UIBezierPath(rect: yourView.bounds).cgPath
yourView.layer.addSublayer(yourViewBorder)
You can also set different types of design using pattern image like below example.
[yourView.layer setBorderWidth:5.0];
[yourView.layer setBorderColor:[[UIColor colorWithPatternImage:[UIImage imageNamed:#"DotedImage.png"]] CGColor]];///just add image name and create image with dashed or doted drawing and add here
Here you've to add <QuartzCore/QuartzCore> framework in the project and import it with below line in YourViewController.m file.
#import <QuartzCore/QuartzCore.h>
For those of you working in Swift, this class extension on UIView makes it easy. This was based on sunshineDev's answer.
extension UIView {
func addDashedBorder() {
let color = UIColor.red.cgColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.round
shapeLayer.lineDashPattern = [6,3]
shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 5).cgPath
self.layer.addSublayer(shapeLayer)
}
}
To use it:
anyView.addDashedBorder()
Here is a UIView subclass that can work for any project, it also works for round views:
import UIKit
class CustomDashedView: UIView {
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = cornerRadius > 0
}
}
#IBInspectable var dashWidth: CGFloat = 0
#IBInspectable var dashColor: UIColor = .clear
#IBInspectable var dashLength: CGFloat = 0
#IBInspectable var betweenDashesSpace: CGFloat = 0
var dashBorder: CAShapeLayer?
override func layoutSubviews() {
super.layoutSubviews()
dashBorder?.removeFromSuperlayer()
let dashBorder = CAShapeLayer()
dashBorder.lineWidth = dashWidth
dashBorder.strokeColor = dashColor.cgColor
dashBorder.lineDashPattern = [dashLength, betweenDashesSpace] as [NSNumber]
dashBorder.frame = bounds
dashBorder.fillColor = nil
if cornerRadius > 0 {
dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
} else {
dashBorder.path = UIBezierPath(rect: bounds).cgPath
}
layer.addSublayer(dashBorder)
self.dashBorder = dashBorder
}
}
This way you can edit from the Storyboard like this:
A pair of results:
Swift 3:
import UIKit
class UIViewWithDashedLineBorder: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath(roundedRect: rect, cornerRadius: 0)
UIColor.purple.setFill()
path.fill()
UIColor.orange.setStroke()
path.lineWidth = 5
let dashPattern : [CGFloat] = [10, 4]
path.setLineDash(dashPattern, count: 2, phase: 0)
path.stroke()
}
}
Use in a storyboard (as custom class) or directly in code:
let v = UIViewWithDashedLineBorder(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
Result:
Building upon what Prasad G has suggested I created a method inside a UIImage Extras class with the following:
- (CAShapeLayer *) addDashedBorderWithColor: (CGColorRef) color {
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
CGSize frameSize = self.size;
CGRect shapeRect = CGRectMake(0.0f, 0.0f, frameSize.width, frameSize.height);
[shapeLayer setBounds:shapeRect];
[shapeLayer setPosition:CGPointMake( frameSize.width/2,frameSize.height/2)];
[shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
[shapeLayer setStrokeColor:color];
[shapeLayer setLineWidth:5.0f];
[shapeLayer setLineJoin:kCALineJoinRound];
[shapeLayer setLineDashPattern:
[NSArray arrayWithObjects:[NSNumber numberWithInt:10],
[NSNumber numberWithInt:5],
nil]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:shapeRect cornerRadius:15.0];
[shapeLayer setPath:path.CGPath];
return shapeLayer;
}
It's important to point out that if you define your shape's position as (0,0), the bottom corner of the border will be placed in the center of the image, that's why I set it to: (frameSize.width/2,frameSize.height/2)
I then use my method to get the dashed border using the UIImage of my UIImageView and add the CAShapeLayer as a sublayer of the UIImageView layer:
[myImageView.layer addSublayer:[myImageView.image addDashedBorderWithColor:[[UIColor whiteColor] CGColor]]];
Use CGContextSetLineDash() method.
CGFloat dashPattern[]= {3.0, 2};
context =UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
// And draw with a blue fill color
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 4.0);
CGContextSetLineDash(context, 0.0, dashPattern, 2);
CGContextAddRect(context, self.bounds);
// Close the path
CGContextClosePath(context);
CGContextStrokePath(context);
// Fill & stroke the path
CGContextDrawPath(context, kCGPathFillStroke);
I think it will be helpful to you.
For this you need add CAShapeLayer for that particular object
CAShapeLayer * dotborder = [CAShapeLayer layer];
dotborder.strokeColor = [UIColor redColor].CGColor;//your own color
dotborder.fillColor = nil;
dotborder.lineDashPattern = #[#4, #2];//your own patten
[codeBtn.layer addSublayer:dotborder];
dotborder.path = [UIBezierPath bezierPathWithRect:codeBtn.bounds].CGPath;
dotborder.frame = codeBtn.bounds;
Swift 4.2
Based off rmooney's answer as a UIView extension with configurable parameters that have default values set.
Note this does not work if the view has self.translatesAutoresizingMaskIntoConstraints = false
extension UIView {
func addDashedBorder(_ color: UIColor = UIColor.black, withWidth width: CGFloat = 2, cornerRadius: CGFloat = 5, dashPattern: [NSNumber] = [3,6]) {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = bounds
shapeLayer.position = CGPoint(x: bounds.width/2, y: bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = color.cgColor
shapeLayer.lineWidth = width
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = dashPattern
shapeLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
self.layer.addSublayer(shapeLayer)
}
}
• Swift 5
• Works with autolayout
• Works with the corner radius
import UIKit
class DashedBorderView: UIView {
private let dashedLineColor = UIColor.black.cgColor
private let dashedLinePattern: [NSNumber] = [6, 3]
private let dashedLineWidth: CGFloat = 4
private let borderLayer = CAShapeLayer()
init() {
super.init(frame: CGRect.zero)
borderLayer.strokeColor = dashedLineColor
borderLayer.lineDashPattern = dashedLinePattern
borderLayer.backgroundColor = UIColor.clear.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.lineWidth = dashedLineWidth
layer.addSublayer(borderLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
borderLayer.frame = bounds
borderLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: layer.cornerRadius).cgPath
}
}
Swift version of the QuartzCore answer.
import QuartzCore
let dottedPattern = UIImage(named: "dottedPattern")
myView.layer.borderWidth = 1
myView.layer.borderColor = UIColor(patternImage: dottedPattern!).CGColor
The CAShapeLayer approach works, but the QuartzCore approach is better at handling a Table View reload, if the UIView is inside a cell.
For the image, you can use something like this (it's really small):
I tend to prefer vector over PNGs when I can get away with it:
Within Sketch, create a 4x4 pixel rectangle.
Make a total of four of these
Group them into a foursquare, alternating colors
Export the group as PDF
Within Images.xcassets, create a New Image Set called dottedPattern
Change the Scale Factors to Single Vector
Drop in your PDF
For Xamarin.iOS dashed/dotted border.
dottedLayer = new CAShapeLayer();
dottedLayer.StrokeColor = UIColor.FromRGB(202, 202, 208).CGColor;
dottedLayer.FillColor = null;
dottedLayer.LineDashPattern = new[] { new NSNumber(4), new NSNumber(2) };
dottedLayer.Path = UIBezierPath.FromRect(YourView.Bounds).CGPath; //for square
dottedLayer.Path = UIBezierPath.FromRoundedRect(YourView.Bounds, 5).CGPath; //for rounded corners
dottedLayer.Frame = YourView.Bounds;
YourView.Layer.AddSublayer(dottedLayer);
In Swift 3
let border = CAShapeLayer();
border.strokeColor = UIColor.black.cgColor;
border.fillColor = nil;
border.lineDashPattern = [4, 4];
border.path = UIBezierPath(rect: theView.bounds).cgPath
border.frame = theView.bounds;
theView.layer.addSublayer(border);
For Swift 5
extension UIView {
func addDashBorder() {
let color = UIColor.white.cgColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
shapeLayer.bounds = shapeRect
shapeLayer.name = "DashBorder"
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 1.5
shapeLayer.lineJoin = .round
shapeLayer.lineDashPattern = [2,4]
shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 10).cgPath
self.layer.masksToBounds = false
self.layer.addSublayer(shapeLayer)
}
}
How to add
vw.addDashBorder()
How to remove border again
let _ = vw.layer.sublayers?.filter({$0.name == "DashBorder"}).map({$0.removeFromSuperlayer()})
This is if you wanted it in Swift 2
func addDashedLineBorderWithColor(color:UIColor) {
let _ = self.sublayers?.filter({$0.name == "DashedBorder"}).map({$0.removeFromSuperlayer()})
let border = CAShapeLayer();
border.name = "DashedBorder"
border.strokeColor = color.CGColor;
border.fillColor = nil;
border.lineDashPattern = [4, 4];
border.path = UIBezierPath(rect: self.bounds).CGPath
border.frame = self.bounds;
self.addSublayer(border);
}
try bellow code
- (void)drawRect:(CGRect)rect {
//// Color Declarations
UIColor* fillColor = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 1];
UIColor* strokeColor = [UIColor colorWithRed: 0.29 green: 0.565 blue: 0.886 alpha: 1];
//// Rectangle Drawing
UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius: 6];
[fillColor setFill];
[rectanglePath fill];
[strokeColor setStroke];
rectanglePath.lineWidth = 1;
CGFloat rectanglePattern[] = {6, 2, 6, 2};
[rectanglePath setLineDash: rectanglePattern count: 4 phase: 0];
[rectanglePath stroke];
[super drawRect:rect];
}
for one like bellow
Swift solution with custom class worked with autolayout
customized from #Iain Smith
class DashedBorderView: UIView {
#IBInspectable var cornerRadius: CGFloat = 4
#IBInspectable var borderColor: UIColor = UIColor.black
#IBInspectable var dashPaintedSize: Int = 2
#IBInspectable var dashUnpaintedSize: Int = 2
let dashedBorder = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
//custom initialization
self.layer.addSublayer(dashedBorder)
applyDashBorder()
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
applyDashBorder()
}
func applyDashBorder() {
dashedBorder.strokeColor = borderColor.cgColor
dashedBorder.lineDashPattern = [NSNumber(value: dashPaintedSize), NSNumber(value: dashUnpaintedSize)]
dashedBorder.fillColor = nil
dashedBorder.cornerRadius = cornerRadius
dashedBorder.path = UIBezierPath(rect: self.bounds).cgPath
dashedBorder.frame = self.bounds
}
}
extension UIView{
func addDashedLineBorder() {
let color = UIColor.black.cgColor
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = (self.frame.size)
let shapeRect = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 1
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [2,2]
shapeLayer.path = UIBezierPath(rect: shapeRect).cgPath
self.layer.addSublayer(shapeLayer)
}
}
and call this function in viewdidLoad() with delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Your code with delay
self.YourView.addDashedBorder()
}
Swift 4,5 :- addDashedBorder
extension UIView {
func setCellDeshBorder(color: UIColor = .appBlue?.withAlphaComponent(0.50) ?? UIColor()) {
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
shapeLayer.bounds = shapeRect
shapeLayer.name = "DashBorder"
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color.cgColor
shapeLayer.lineWidth = 1.5
shapeLayer.lineJoin = .round
shapeLayer.lineDashPattern = [2,4]
shapeLayer.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 10).cgPath
self.layer.masksToBounds = false
self.layer.addSublayer(shapeLayer)
}
func removeCellDeshBorder() {
_ = self.layer.sublayers?.filter({$0.name == "DashBorder"}).map({$0.removeFromSuperlayer()})
}
}
I ended up creating a IB Designable using some of #Chris implementation:
CurvedDashedBorderUIVIew.h:
#import <UIKit/UIKit.h>
IB_DESIGNABLE
#interface CurvedDashedBorderUIVIew : UIView
#property (nonatomic) IBInspectable CGFloat cornerRadius;
#property (nonatomic) IBInspectable UIColor *borderColor;
#property (nonatomic) IBInspectable int dashPaintedSize;
#property (nonatomic) IBInspectable int dashUnpaintedSize;
#property (strong, nonatomic) CAShapeLayer *border;
#end
CurvedDashedBorderUIVIew.m:
#import "CurvedDashedBorderUIVIew.h"
#implementation CurvedDashedBorderUIVIew
- (instancetype)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
-(void)setup
{
_border = [CAShapeLayer layer];
[self.layer addSublayer:_border];
}
-(void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.cornerRadius;
_border.strokeColor = self.borderColor.CGColor;
_border.fillColor = nil;
_border.lineDashPattern = #[[NSNumber numberWithInt:_dashPaintedSize],
[NSNumber numberWithInt:_dashUnpaintedSize]];
_border.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius].CGPath;
_border.frame = self.bounds;
}
#end
then just set it up in the xib/storyboard:
You can simply create a IBDesignable class like this:
import UIKit
#IBDesignable
class BorderedView: UIView {
#IBInspectable var cornerRadius: CGFloat = 0
#IBInspectable var borderWidth: CGFloat = 0
#IBInspectable var borderColor: UIColor = UIColor.clear
override func draw(_ rect: CGRect) {
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
path.lineWidth = borderWidth
borderColor.setStroke()
let dashPattern : [CGFloat] = [10, 4]
path.setLineDash(dashPattern, count: 2, phase: 0)
path.stroke()
}
}
Then just subclass your view with BorderedView from Xcode.
This way you can set the border color and border width very easily from the interface builder!
Swift 5+
import UIKit
class DashedBorderView: UIView {
private let borderLayer = CAShapeLayer()
init(color: UIColor, width: CGFloat = 1) {
super.init(frame: CGRect.zero)
let pattern: [NSNumber] = [NSNumber(value: Float(5 * width)), NSNumber(value: Float(3 * width))]
borderLayer.backgroundColor = nil
borderLayer.fillColor = nil
borderLayer.lineDashPattern = pattern
borderLayer.lineWidth = width
borderLayer.strokeColor = color.cgColor
layer.addSublayer(borderLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
borderLayer.frame = bounds
borderLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: layer.cornerRadius).cgPath
}
}
How to use:
// f.e. inside UIViewController
let viewWithDashedBorder = DashedBorderView(color: .red, width: 2)
view.addSubview(viewWithDashedBorder)
In swift 4 I created an UIView extension with the following function:
func borderDash(withRadius cornerRadius: Float, borderWidth: Float, borderColor: UIColor, dashSize: Int) {
let currentFrame = self.bounds
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
let radius = CGFloat(cornerRadius)
// Points - Eight points that define the round border. Each border is defined by two points.
let topLeftPoint = CGPoint(x: radius, y: 0)
let topRightPoint = CGPoint(x: currentFrame.size.width - radius, y: 0)
let middleRightTopPoint = CGPoint(x: currentFrame.size.width, y: radius)
let middleRightBottomPoint = CGPoint(x: currentFrame.size.width, y: currentFrame.size.height - radius)
let bottomRightPoint = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height)
let bottomLeftPoint = CGPoint(x: radius, y: currentFrame.size.height)
let middleLeftBottomPoint = CGPoint(x: 0, y: currentFrame.size.height - radius)
let middleLeftTopPoint = CGPoint(x: 0, y: radius)
// Points - Four points that are the center of the corners borders.
let cornerTopRightCenter = CGPoint(x: currentFrame.size.width - radius, y: radius)
let cornerBottomRightCenter = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height - radius)
let cornerBottomLeftCenter = CGPoint(x: radius, y: currentFrame.size.height - radius)
let cornerTopLeftCenter = CGPoint(x: radius, y: radius)
// Angles - The corner radius angles.
let topRightStartAngle = CGFloat(Double.pi * 3 / 2)
let topRightEndAngle = CGFloat(0)
let bottomRightStartAngle = CGFloat(0)
let bottmRightEndAngle = CGFloat(Double.pi / 2)
let bottomLeftStartAngle = CGFloat(Double.pi / 2)
let bottomLeftEndAngle = CGFloat(Double.pi)
let topLeftStartAngle = CGFloat(Double.pi)
let topLeftEndAngle = CGFloat(Double.pi * 3 / 2)
// Drawing a border around a view.
path.move(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addArc(center: cornerTopRightCenter,
radius: radius,
startAngle: topRightStartAngle,
endAngle: topRightEndAngle,
clockwise: false)
path.addLine(to: middleRightBottomPoint)
path.addArc(center: cornerBottomRightCenter,
radius: radius,
startAngle: bottomRightStartAngle,
endAngle: bottmRightEndAngle,
clockwise: false)
path.addLine(to: bottomLeftPoint)
path.addArc(center: cornerBottomLeftCenter,
radius: radius,
startAngle: bottomLeftStartAngle,
endAngle: bottomLeftEndAngle,
clockwise: false)
path.addLine(to: middleLeftTopPoint)
path.addArc(center: cornerTopLeftCenter,
radius: radius,
startAngle: topLeftStartAngle,
endAngle: topLeftEndAngle,
clockwise: false)
// Path is set as the shapeLayer object's path.
shapeLayer.path = path;
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.frame = currentFrame
shapeLayer.masksToBounds = false
shapeLayer.setValue(0, forKey: "isCircle")
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = borderColor.cgColor
shapeLayer.lineWidth = CGFloat(borderWidth)
shapeLayer.lineDashPattern = [NSNumber(value: dashSize), NSNumber(value: dashSize)]
shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(shapeLayer)
self.layer.cornerRadius = radius;
}
If you want this to work with cornerRadius then try this
tagView.clipsToBounds = YES;
tagView.layer.cornerRadius = 20.0f;
tagView.backgroundColor = [UIColor groupTableViewBackgroundColor];
CAShapeLayer *yourViewBorder = [CAShapeLayer layer];
yourViewBorder.strokeColor = [UIColor blackColor].CGColor;
yourViewBorder.fillColor = nil;
yourViewBorder.lineDashPattern = #[#2, #2];
yourViewBorder.frame = tagView.bounds;
// Create the path for to make circle
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:tagView.bounds
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(20, 20)];
yourViewBorder.path = maskPath.CGPath;
[tagView.layer addSublayer:yourViewBorder];
Improvement for #Chris's answer.
extension UIView {
func dashLinedBorder() -> CAShapeLayer {
let viewBorder = CAShapeLayer()
viewBorder.strokeColor = UIColor.black.cgColor
viewBorder.lineDashPattern = [4, 2]
viewBorder.fillColor = nil
self.layer.addSublayer(viewBorder)
return viewBorder
}
}
Define your CAShapeLayer inside the ViewController,
var viewBillingProofCAShapeLayer: CAShapeLayer!
override func viewDidLoad() {
self.viewBillingProofCAShapeLayer = self.viewBillingProofInner.dashLinedBorder()
}
then override viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
self.viewBillingProofCAShapeLayer.path = UIBezierPath(roundedRect: self.viewBillingProofInner.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 4, height: 4)).cgPath
self.viewBillingProofCAShapeLayer.frame = self.viewBillingProofInner.bounds
}
SwiftUI
var body: some View {
Rectangle()
.strokeBorder(style: StrokeStyle(lineWidth: 4, dash: [10]))
}

Resources