Properly animating circle radius with CABasicAnimation - ios

I want to expand and contract a circle using CABasicAnimation. I previously designed a clean and effective way to draw the animation using sin and the drawRect method of the UIView class, but unfortunately it seems you can't animate or force drawRect to fire in UIView.animate methods no matter what you do. Therefore, I've had to come up with a workaround using CABasicAnimation. The result kind of works, but the animation is ugly and doesn't look right. Here's an illustration of what I mean:
Warping occurs during the expansion and contraction, and there appears to be a dent or cavity on the right side when the circle is expanding. Here's the class responsible for animating it:
struct PathConfig {
var arcCenter: CGPoint
var radius: CGFloat
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = CGFloat(2 * M_PI)
}
class RGSStandbyIndicatorView: UIView {
// MARK: - Variables & Constants
/// The drawing layer.
var shapeLayer: CAShapeLayer!
// MARK: - Class Methods
/// Animation function.
func animate(period: CFTimeInterval) {
let p: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 17.5)
let b: UIBezierPath = UIBezierPath(arcCenter: p.arcCenter, radius: p.radius, startAngle: p.startAngle, endAngle: p.endAngle, clockwise: true)
let a: CABasicAnimation = CABasicAnimation(keyPath: "path")
a.fromValue = shapeLayer.path
a.toValue = b.cgPath
a.autoreverses = true
a.duration = period
a.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
a.repeatCount = HUGE
shapeLayer.add(a, forKey: "animateRadius")
}
/// Initializes and returns a CAShapeLayer with the given draw path and fill color.
private class func shapeLayerForPath(_ path: CGPath, with fillColor: CGColor) -> CAShapeLayer {
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = path
shapeLayer.fillColor = fillColor
return shapeLayer
}
/// Configures the UIView
private func setup() {
// Init config, path.
let cfg: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 0.0)
let path: UIBezierPath = UIBezierPath(arcCenter: cfg.arcCenter, radius: cfg.radius, startAngle: cfg.startAngle, endAngle: cfg.endAngle, clockwise: true)
// Init CAShapeLayer
shapeLayer = RGSStandbyIndicatorView.shapeLayerForPath(path.cgPath, with: UIColor.white.cgColor)
layer.addSublayer(shapeLayer)
}
// MARK: Class Method Overrides
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
I've tried adjusting the start and end angles hoping it was some artifact of the path having no overlap, but it didn't change anything. I'm not sure how to fix this problem, and It's based off Chadwick Wood's answer to a similar question.

Use this UIView subclass
class RadioAnimationView: UIView {
var animatableLayer : CAShapeLayer?
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = self.bounds.height/2
self.animatableLayer = CAShapeLayer()
self.animatableLayer?.fillColor = self.backgroundColor?.cgColor
self.animatableLayer?.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
self.animatableLayer?.frame = self.bounds
self.animatableLayer?.cornerRadius = self.bounds.height/2
self.animatableLayer?.masksToBounds = true
self.layer.addSublayer(self.animatableLayer!)
self.startAnimation()
}
func startAnimation()
{
let layerAnimation = CABasicAnimation(keyPath: "transform.scale")
layerAnimation.fromValue = 1
layerAnimation.toValue = 3
layerAnimation.isAdditive = false
layerAnimation.duration = CFTimeInterval(2)
layerAnimation.fillMode = kCAFillModeForwards
layerAnimation.isRemovedOnCompletion = true
layerAnimation.repeatCount = .infinity
layerAnimation.autoreverses = true
self.animatableLayer?.add(layerAnimation, forKey: "growingAnimation")
}
}
Hope this helps you

Related

Animation in swift : Circular Dashed Loading Bar?

I have literally looked over the web for like 2to3 hours, unfortunately couldn't find anything, (2nd problem: being new to swift ->this can be ignored)
Basically I need Circular dashed Loading bar : Every dot should animate Individually like a circular load..
I was able to make a circular dashed circle but couldn't animate it...
Any help would be appreciated even if thats an library from github or anything..
Here's my code for creating a circular dashed circle..
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: 100.0, startAngle: 0.0, endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.position = CGPoint(x: 100, y: 100)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 6.0
let one : NSNumber = 1
let two : NSNumber = 13
shapeLayer.lineDashPattern = [one,two]
shapeLayer.lineCap = kCALineCapRound
view.layer.addSublayer(shapeLayer)
}
override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning()}
}
This is what I want (Below One):
I have been playing around with custom activity indicators, so here's one way to do it
import UIKit
#IBDesignable class ActivityIndicatorCircle: UIView {
var timerInterval: Double = 0.1
var timer : Timer?
var endAngle: CGFloat = 0.0
var angleStep = CGFloat.pi / 20.0
var angleOffset = -CGFloat.pi / 2.0
var shapeLayer = CAShapeLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func startAnimating() {
superview?.bringSubview(toFront: self)
layer.cornerRadius = frame.width / 2
self.clipsToBounds = true
isHidden = false
timer?.invalidate()
timer = nil
timer = Timer.scheduledTimer(timeInterval: timerInterval,
target: self,
selector: #selector(self.updateCircle),
userInfo: nil,
repeats: true)
}
func stopAnimating() {
isHidden = true
timer?.invalidate()
timer = nil
}
func updateCircle() {
endAngle += angleStep
if endAngle > CGFloat.pi * 2.0 {
endAngle -= CGFloat.pi * 2.0
}
DispatchQueue.main.async {
self.shapeLayer.removeFromSuperlayer() // remove the previous version
let lineWidth: CGFloat = 6.0
let radius = self.frame.size.width / 2.0 // if the view is square, this gives us center as well
let circlePath = UIBezierPath(arcCenter: CGPoint(x: radius,y: radius), radius: radius - lineWidth, startAngle: self.angleOffset, endAngle: self.endAngle + self.angleOffset, clockwise: true)
self.shapeLayer.path = circlePath.cgPath
self.shapeLayer.position = CGPoint(x: 0, y: 0)
self.shapeLayer.fillColor = UIColor.clear.cgColor
self.shapeLayer.strokeColor = UIColor.blue.cgColor
self.shapeLayer.lineWidth = lineWidth
let one : NSNumber = 1
let two : NSNumber = 13
self.shapeLayer.lineDashPattern = [one,two]
self.shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(self.shapeLayer)
}
}
}
to use this, add a UIView to your storyboard, and set the class to ActivityIndicatorCircle
To start it, call activityIndicatorCircle.startAnimating()
for circular loader you can use this library it will help you. https://github.com/naoyashiga/RPLoadingAnimation

add and remove a subview using a button

Hey guys so I've drawn a circle and added it as a subview using a button. When I try to remove it using the removefromsuperview it didn't go away. Please check the code below.
I added a button named Removecircle so I can add or remove the circle but that didn't go as planned.
import UIKit
import GLKit
class ViewController: UIViewController {
var numb = 0
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func Removecircle(_ sender: Any) {
let radius = Int( view.frame.maxX )
let HvalueX = 0
let HvalueY = Int( view.frame.maxY )/2 - Int( view.frame.maxX )/2
// Create a new CircleView
let circleView = CircleView( frame:CGRect(x: HvalueX, y: HvalueY, width: radius, height: radius ))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
if numb%2 == 0 {
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 4.0)
circleView.circleLayer.strokeColor = UIColor.blue.cgColor
}else if numb%2 == 1 {
circleView.removeFromSuperview()
}
numb = numb + 1
}
#IBOutlet weak var removecircle: UIButton!
}
class CircleView: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
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.white.cgColor
circleLayer.lineWidth = 3.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
animateCircle(duration: 2)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
You’re creating a new CircleView every time you press the button. Instead, create it the first time and store it as a property of ViewController. Then when you press the button you should be able to remove it with removeFromSuperview().

Argument passed to call that takes no arguments

I am adding in a animated circle to a uiview. In this line of code:
var circleView = addCircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
I am getting an error that reads "Argument passed to call that takes no arguments" pointing to CGRectMake.
I have aisles attached the rest of the code incase it is needed
import UIKit
import CoreMotion
import CoreGraphics
class Animation: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
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.red.cgColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
var circleView = addCircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
UIView.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
}
}
Credits to Mike S
While I'd change all references of CGRectMake to CGRect, the issue is with the call to addCircle(). You didn't define any arguments.
Try changing things to:
func addCircleView(frame: CGRect) {
Or, since addCircleView looks like it doesn't use this parameter, try removing the CGRect/CGRectMake from the call to addCircle():
var circleView = addCircleView()
(It looks like you probably want the former.)

Circle Layer error with super.init - Swift

This is going to be a really basic question.
I am working through this answer:
Animate drawing of a circle
But no matter how I format it I get an error. I can see from the error that I have not initialized circle and I am sure it is just a positioning thing but not sure what or how to do that correctly or what is wrong with how I have layout.
When I try like this i get the error ('self.circleLayer' not initialized at super.init call):
import UIKit
class CircleView: UIView {
let circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
// 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.clearColor().CGColor
circleLayer.strokeColor = UIColor.redColor().CGColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then tried moving it to after the initializer like this which doesn't give me an error):
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let circleLayer: CAShapeLayer!
self.backgroundColor = UIColor.clearColor()
// 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.clearColor().CGColor
circleLayer.strokeColor = UIColor.redColor().CGColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
But then when I try to put a function in my viewcontroller.swift that references circleLayer I get unresolved identifier:
func animateCircle(duration: NSTimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.addAnimation(animation, forKey: "animateCircle")
}
I am sure it just something really simple but I am not sure what.
Thanks for your help.
From the documentation
Safety check 1
A designated initializer must ensure that all of the
properties introduced by its class are initialized before it delegates
up to a superclass initializer.
Initialize circleLayer in the declaration line and move self.backgroundColor = ... after super.init
class CircleView: UIView {
let circleLayer = CAShapeLayer()
override init(frame: CGRect) {
// 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)
super.init(frame: frame)
// Setup the CAShapeLayer with the path, colors, and line width
self.backgroundColor = UIColor.clearColor()
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.strokeColor = UIColor.redColor().CGColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How do I draw a circle in iOS Swift?

let block = UIView(frame: CGRectMake(cellWidth-25, cellHeight/2-8, 16, 16))
block.backgroundColor = UIColor(netHex: 0xff3b30)
block.layer.cornerRadius = 9
block.clipsToBounds = true
This is what I have right now, but it's obviously not the right way to do it.
What's the simplest way to do it?
Alert. This old answer is absolutely incorrect.
WARNING! This is an incorrect solution. layers are added infinitely in the drawRect method (every time the view is drawn). You should NEVER add layers in the drawRect method. Use layoutSubview instead.
You can draw a circle with this (Swift 3.0+):
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), radius: CGFloat(20), startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
// Change the fill color
shapeLayer.fillColor = UIColor.clear.cgColor
// You can change the stroke color
shapeLayer.strokeColor = UIColor.red.cgColor
// You can change the line width
shapeLayer.lineWidth = 3.0
view.layer.addSublayer(shapeLayer)
With the code you have posted you are cropping the corners of the UIView, not adding a circle to the view.
Here's a full example of using that method:
/// A special UIView displayed as a ring of color
class Ring: UIView {
override func drawRect(rect: CGRect) {
drawRingFittingInsideView()
}
internal func drawRingFittingInsideView() -> () {
let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)
let desiredLineWidth:CGFloat = 1 // your desired value
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:halfSize,y:halfSize),
radius: CGFloat( halfSize - (desiredLineWidth/2) ),
startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.lineWidth = desiredLineWidth
layer.addSublayer(shapeLayer)
}
}
Note, however there's an incredibly handy call:
let circlePath = UIBezierPath(ovalInRect: rect)
which does all the work of making the path. (Don't forget to inset it for the line thickness, which is also incredibly easy with CGRectInset.)
internal func drawRingFittingInsideView(rect: CGRect) {
let desiredLineWidth:CGFloat = 4 // Your desired value
let hw:CGFloat = desiredLineWidth/2
let circlePath = UIBezierPath(ovalInRect: CGRectInset(rect,hw,hw))
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.lineWidth = desiredLineWidth
layer.addSublayer(shapeLayer)
}
In practice these days in Swift, you would certainly use #IBDesignable and #IBInspectable. Using these you can actually see and change the rendering, in Storyboard!
As you can see, it actually adds new features to the Inspector on the Storyboard, which you can change on the Storyboard:
/// A dot with a border, which you can control completely in Storyboard
#IBDesignable class Dot: UIView {
#IBInspectable var mainColor: UIColor = UIColor.blueColor() {
didSet {
print("mainColor was set here")
}
}
#IBInspectable var ringColor: UIColor = UIColor.orangeColor() {
didSet {
print("bColor was set here")
}
}
#IBInspectable var ringThickness: CGFloat = 4 {
didSet {
print("ringThickness was set here")
}
}
#IBInspectable var isSelected: Bool = true
override func drawRect(rect: CGRect) {
let dotPath = UIBezierPath(ovalInRect:rect)
let shapeLayer = CAShapeLayer()
shapeLayer.path = dotPath.CGPath
shapeLayer.fillColor = mainColor.CGColor
layer.addSublayer(shapeLayer)
if (isSelected) {
drawRingFittingInsideView(rect)
}
}
internal func drawRingFittingInsideView(rect: CGRect) {
let hw:CGFloat = ringThickness/2
let circlePath = UIBezierPath(ovalInRect: CGRectInset(rect,hw,hw) )
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = ringColor.CGColor
shapeLayer.lineWidth = ringThickness
layer.addSublayer(shapeLayer)
}
}
Finally, note that if you have a UIView (which is square, and which you set to say red in Storyboard) and you simply want to turn it in to a red circle, you can just do the following:
// Makes a UIView into a circular dot of color
class Dot: UIView {
override func layoutSubviews() {
layer.cornerRadius = bounds.size.width/2
}
}
Make a class UIView and assign it this code for a simple circle
import UIKit
#IBDesignable
class DRAW: UIView {
override func draw(_ rect: CGRect) {
var path = UIBezierPath()
path = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100))
UIColor.yellow.setStroke()
UIColor.red.setFill()
path.lineWidth = 5
path.stroke()
path.fill()
}
}
If you want to use a UIView to draw it, then you need to make the radius / of the height or width.
so just change:
block.layer.cornerRadius = 9
to:
block.layer.cornerRadius = block.frame.width / 2
You'll need to make the height and width the same however. If you'd like to use coregraphics, then you'll want to do something like this:
CGContextRef ctx= UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
CGPoint center;
center.x = bounds.origin.x + bounds.size.width / 2.0;
center.y = bounds.origin.y + bounds.size.height / 2.0;
CGContextSaveGState(ctx);
CGContextSetLineWidth(ctx,5);
CGContextSetRGBStrokeColor(ctx,0.8,0.8,0.8,1.0);
CGContextAddArc(ctx,locationOfTouch.x,locationOfTouch.y,30,0.0,M_PI*2,YES);
CGContextStrokePath(ctx);
Here is my version using Swift 5 and Core Graphics.
I have created a class to draw two circles. The first circle is created using addEllipse(). It puts the ellipse into a square, thus creating a circle. I find it surprising that there is no function addCircle(). The second circle is created using addArc() of 2pi radians
import UIKit
#IBDesignable
class DrawCircles: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
print("could not get graphics context")
return
}
context.setLineWidth(2)
context.setStrokeColor(UIColor.blue.cgColor)
context.addEllipse(in: CGRect(x: 30, y: 30, width: 50.0, height: 50.0))
context.strokePath()
context.setStrokeColor(UIColor.red.cgColor)
context.beginPath() // this prevents a straight line being drawn from the current point to the arc
context.addArc(center: CGPoint(x:100, y: 100), radius: 20, startAngle: 0, endAngle: 2.0*CGFloat.pi, clockwise: false)
context.strokePath()
}
}
in your ViewController's didViewLoad() add the following:
let myView = DrawCircles(frame: CGRect(x: 50, y: 50, width: 300, height: 300))
self.view.addSubview(myView)
When it runs it should look like this. I hope you like my solution!
Swift 4 version of accepted answer:
#IBDesignable
class CircledDotView: UIView {
#IBInspectable var mainColor: UIColor = .white {
didSet { print("mainColor was set here") }
}
#IBInspectable var ringColor: UIColor = .black {
didSet { print("bColor was set here") }
}
#IBInspectable var ringThickness: CGFloat = 4 {
didSet { print("ringThickness was set here") }
}
#IBInspectable var isSelected: Bool = true
override func draw(_ rect: CGRect) {
let dotPath = UIBezierPath(ovalIn: rect)
let shapeLayer = CAShapeLayer()
shapeLayer.path = dotPath.cgPath
shapeLayer.fillColor = mainColor.cgColor
layer.addSublayer(shapeLayer)
if (isSelected) {
drawRingFittingInsideView(rect: rect)
}
}
internal func drawRingFittingInsideView(rect: CGRect) {
let hw: CGFloat = ringThickness / 2
let circlePath = UIBezierPath(ovalIn: rect.insetBy(dx: hw, dy: hw))
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = ringColor.cgColor
shapeLayer.lineWidth = ringThickness
layer.addSublayer(shapeLayer)
}
}
Updating #Dario's code approach for Xcode 8.2.2, Swift 3.x. Noting that in storyboard, set the Background color to "clear" to avoid a black background in the square UIView:
import UIKit
#IBDesignable
class Dot:UIView
{
#IBInspectable var mainColor: UIColor = UIColor.clear
{
didSet { print("mainColor was set here") }
}
#IBInspectable var ringColor: UIColor = UIColor.clear
{
didSet { print("bColor was set here") }
}
#IBInspectable var ringThickness: CGFloat = 4
{
didSet { print("ringThickness was set here") }
}
#IBInspectable var isSelected: Bool = true
override func draw(_ rect: CGRect)
{
let dotPath = UIBezierPath(ovalIn: rect)
let shapeLayer = CAShapeLayer()
shapeLayer.path = dotPath.cgPath
shapeLayer.fillColor = mainColor.cgColor
layer.addSublayer(shapeLayer)
if (isSelected) { drawRingFittingInsideView(rect: rect) }
}
internal func drawRingFittingInsideView(rect: CGRect)->()
{
let hw:CGFloat = ringThickness/2
let circlePath = UIBezierPath(ovalIn: rect.insetBy(dx: hw,dy: hw) )
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = ringColor.cgColor
shapeLayer.lineWidth = ringThickness
layer.addSublayer(shapeLayer)
}
}
And if you want to control the start and end angles:
import UIKit
#IBDesignable
class Dot:UIView
{
#IBInspectable var mainColor: UIColor = UIColor.clear
{
didSet { print("mainColor was set here") }
}
#IBInspectable var ringColor: UIColor = UIColor.clear
{
didSet { print("bColor was set here") }
}
#IBInspectable var ringThickness: CGFloat = 4
{
didSet { print("ringThickness was set here") }
}
#IBInspectable var isSelected: Bool = true
override func draw(_ rect: CGRect)
{
let dotPath = UIBezierPath(ovalIn: rect)
let shapeLayer = CAShapeLayer()
shapeLayer.path = dotPath.cgPath
shapeLayer.fillColor = mainColor.cgColor
layer.addSublayer(shapeLayer)
if (isSelected) { drawRingFittingInsideView(rect: rect) }
}
internal func drawRingFittingInsideView(rect: CGRect)->()
{
let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)
let desiredLineWidth:CGFloat = ringThickness // your desired value
let circlePath = UIBezierPath(
arcCenter: CGPoint(x: halfSize, y: halfSize),
radius: CGFloat( halfSize - (desiredLineWidth/2) ),
startAngle: CGFloat(0),
endAngle:CGFloat(Double.pi),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = ringColor.cgColor
shapeLayer.lineWidth = ringThickness
layer.addSublayer(shapeLayer)
}
}
A much easier and resource friendly approach would be.
import UIKit
#IBDesignable
class CircleDrawView: UIView {
#IBInspectable var borderColor: UIColor = UIColor.red;
#IBInspectable var borderSize: CGFloat = 4
override func draw(_ rect: CGRect)
{
layer.borderColor = borderColor.cgColor
layer.borderWidth = borderSize
layer.cornerRadius = self.frame.height/2
}
}
With Border Color and Border Size and the default Background property you can define the appearance of the circle.
Please note, to draw a circle the view's height and width have to be equal in size.
The code is working for Swift >= 4 and Xcode >= 9.
I find Core Graphics to be pretty simple for Swift 3:
if let cgcontext = UIGraphicsGetCurrentContext() {
cgcontext.strokeEllipse(in: CGRect(x: center.x-diameter/2, y: center.y-diameter/2, width: diameter, height: diameter))
}
A simple function drawing a circle on the middle of your window frame, using a multiplicator percentage
/// CGFloat is a multiplicator from self.view.frame.width
func drawCircle(withMultiplicator coefficient: CGFloat) {
let radius = self.view.frame.width / 2 * coefficient
let circlePath = UIBezierPath(arcCenter: self.view.center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.darkGray.cgColor
shapeLayer.lineWidth = 2.0
view.layer.addSublayer(shapeLayer)
}
Add in view did load
//Circle Points
var CircleLayer = CAShapeLayer()
let center = CGPoint (x: myCircleView.frame.size.width / 2, y: myCircleView.frame.size.height / 2)
let circleRadius = myCircleView.frame.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI * 4), clockwise: true)
CircleLayer.path = circlePath.cgPath
CircleLayer.strokeColor = UIColor.red.cgColor
CircleLayer.fillColor = UIColor.blue.cgColor
CircleLayer.lineWidth = 8
CircleLayer.strokeStart = 0
CircleLayer.strokeEnd = 1
Self.View.layer.addSublayer(CircleLayer)
2022, General example of how to actually draw using draw in a UIView.
It's not so easy to properly use UIView#draw.
General beginner tips, you can only draw inside a UIView, it is meaningless otherwise. Further, you can only use the draw commands (.fillEllipse etc) inside the draw call of a UIView.
You almost certainly want to set the intrinsic content size properly. It's important to fully understand how to use this on consumers views, in the two possible situations (a) you are using constraints (b) you are positioning the view by hand in layoutSubviews inside another view.
A huge gotchya is that you cannot draw outside the frame, no matter what. In contrast if you just use lazy vars with a layer to draw a shape (whether dot, circle, etc) it's no problem if you go outside the nominal frame (indeed you often just make the frame size zero so that everything centers easily in your consumer code). But once you start using draw you MUST be inside the frame. This is often confusing as in some cases you "don't know how big your drawing is going to be" until you draw it.
A huge gotchya is, when you are drawing either circles or edges, beginner programmers accidentally cut off half the thickness of that line, due to the fact that draw absolutely can't draw outside the frame. You have to inset the circle or rectangle, by, half the width of the line thickness.
Some code with correct 2022 syntax:
import UIKit
class ExampleDot: UIIView {
// setup ..
// clipsToBounds = false BUT SEE NOTES
// backgroundColor = .clear
override var intrinsicContentSize: CGSize {
return CGSize(width: 40, height: 40)
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// example of a dot
ctx.setFillColor(UIColor.black.cgColor)
ctx.fillEllipse(in: CGRect(x: 0, y: 0, width: 40, height: 40))
// example of a round circle BUT SEE NOTES
ctx.setStrokeColor(UIColor.systemYellow.cgColor)
ctx.setLineWidth(2)
ctx.strokeEllipse(in: CGRect(x: 1, y: 1, width: 40 - 4, height: 40 - 4))
// example of a smaller inner dot
ctx.setFillColor(UIColor.white.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 20, height: 20))
}
}

Resources