Swift: rainbow colour circle - ios

Hi i am trying to write colour picker in swift that looks like this.
But so far I managed this.
Draw circle was easy, heres code...
fileprivate func setupScene(){
let circlePath: UIBezierPath = UIBezierPath(arcCenter: CGPoint(x: self.wheelView.frame.width/2, y: self.wheelView.frame.height/2), radius: CGFloat(self.wheelView.frame.height/2), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//color inside circle
shapeLayer.fillColor = UIColor.clear.cgColor
//colored border of circle
shapeLayer.strokeColor = UIColor.purple.cgColor
//width size of border
shapeLayer.lineWidth = 10
wheelView.layer.addSublayer(shapeLayer)
}
#IBOutlet var wheelView: UIView!
But now I don't know how to insert rainbow colours ... I tried CAGradientLayer but it was not visible. Any good advice?

Details
Xcode 9.1, swift 4
Xcode 10.2.1 (10E1001), Swift 5
Solution
The code was taken from https://github.com/joncardasis/ChromaColorPicker
import UIKit
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
//Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
context.translateBy(x: -self.bounds.midX, y: -self.bounds.midY) //Move context back to original position
context.restoreGState()
}
}
Usage
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rainbowCircle = RainbowCircle(frame: CGRect(x: 50, y: 50, width: 240, height: 420), lineHeight: 5)
rainbowCircle.backgroundColor = .clear
view.addSubview(rainbowCircle)
}
}
Result

Related

Trim UIView with 2 arcs

I have a UIView and I want to trim it with two circles, like I've drawn(sorry for the quality).
My code:
final class TrimmedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let size = CGSize(width: 70, height: 70)
let innerRadius: CGFloat = 366.53658283002471
let innerBottomRadius: CGFloat = 297.88543112651564
let path = UIBezierPath()
path.move(to: CGPoint(x: -innerRadius + (size.width / 2), y: innerRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerRadius), radius: innerRadius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
path.move(to: CGPoint(x: -innerBottomRadius + (size.width / 2), y: innerBottomRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerBottomRadius), radius: innerBottomRadius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.shadowPath = path.cgPath
layer.mask = shapeLayer
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let view = UIView(frame: CGRect(origin: CGPoint(x: (self.view.bounds.width - 70) / 2, y: (self.view.bounds.height - 70) / 2), size: CGSize(width: 70, height: 70)))
view.backgroundColor = .red
self.view.addSubview(view)
let view1 = TrimmedView(frame: view.frame)
view1.backgroundColor = .yellow
self.view.addSubview(view1)
}
I got this result. It seems for me that top trimming works but the bottom doesn't and I don't know why. Any help would be appreciated. Thanks.
Here is a custom view that should give you what you want.
The UIBezierPath uses QuadCurves for the top "convex" arc and the bottom "concave" arc.
It is marked #IBDesignable so you can see it at design-time in IB / Storyboard. The "height" of the arc and the fill color are each set as #IBInspectable so you can adjust those values at design-time as well.
To use it in Storyboard:
Add a normal UIView
change the Class to BohdanShapeView
in the Attributes Inspector pane, set the Arc Offset and the Fill Color
set the background color as with a normal view (you'll probably use clear)
Result:
To use it via code:
let view1 = BohdanShapeView(frame: view.frame)
view1.fillColor = .systemTeal
view1.arcOffset = 10
self.view.addSubview(view1)
Here is the class:
#IBDesignable
class BohdanShapeView: UIView {
#IBInspectable var arcOffset: CGFloat = 0.0
#IBInspectable var fillColor: UIColor = UIColor.white
let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the shape layer
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// fill color for the shape
shapeLayer.fillColor = self.fillColor.cgColor
let width = bounds.size.width
let height = bounds.size.height
let bezierPath = UIBezierPath()
// start at arcOffset below top-left
bezierPath.move(to: CGPoint(x: 0.0, y: 0.0 + arcOffset))
// add curve to arcOffset below top-right
bezierPath.addQuadCurve(to: CGPoint(x: width, y: 0.0 + arcOffset), controlPoint: CGPoint(x: width * 0.5, y: 0.0 - arcOffset))
// add line to bottom-right
bezierPath.addLine(to: CGPoint(x: width, y: height))
// add curve to bottom-left
bezierPath.addQuadCurve(to: CGPoint(x: 0.0, y: height), controlPoint: CGPoint(x: width * 0.5, y: height - arcOffset * 2.0))
// close the path
bezierPath.close()
shapeLayer.path = bezierPath.cgPath
}
}

Suggestions regarding UIImageView customization

I'm trying to figure out the best way to recreate this image in code. I've thought about taking two UIImageViews and connecting them via constraints but that would only get me 50% of the way there because there wouldn't be a diagonal white line splitting the two unique colors. I also want to be able to programmatically change the color of each half of the UIImageView.
My class was very similar to what "May Rest in Peace" posted, but since I had put it together already, I'll go ahead and post it.
The main difference is that I implemented #IBDesignable and #IBInspectable so you can see it and make adjustments in Storyboard / IB
#IBDesignable
class AaronView: UIView {
let leftLayer: CAShapeLayer = CAShapeLayer()
let rightLayer: CAShapeLayer = CAShapeLayer()
let maskLayer: CAShapeLayer = CAShapeLayer()
#IBInspectable
var leftColor: UIColor = UIColor(red: 0.5, green: 0.6, blue: 0.8, alpha: 1.0) {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var rightColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.4, alpha: 1.0) {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divColor: UIColor = UIColor.white {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divAngle: CGFloat = 5.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divWidth: CGFloat = 8.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var radius: CGFloat = 32.0 {
didSet {
setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(leftLayer)
layer.addSublayer(rightLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let x1 = bounds.minX
let y1 = bounds.minY
let x2 = bounds.maxX
let y2 = bounds.maxY
var path = UIBezierPath()
let offset = (bounds.width / 2) * tan(divAngle * CGFloat.pi / 180)
path.move(to: CGPoint(x: x1, y: y1))
path.addLine(to: CGPoint(x: x2 / 2.0 - divWidth / 2.0 + offset, y: y1))
path.addLine(to: CGPoint(x: x2 / 2.0 - divWidth / 2.0 - offset, y: y2))
path.addLine(to: CGPoint(x: x1, y: y2))
path.close()
leftLayer.path = path.cgPath
path = UIBezierPath()
path.move(to: CGPoint(x: x2 / 2.0 + divWidth / 2.0 + offset, y: y1))
path.addLine(to: CGPoint(x: x2, y: y1))
path.addLine(to: CGPoint(x: x2, y: y2))
path.addLine(to: CGPoint(x: x2 / 2.0 + divWidth / 2.0 - offset, y: y2))
path.close()
rightLayer.path = path.cgPath
leftLayer.fillColor = leftColor.cgColor
rightLayer.fillColor = rightColor.cgColor
maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
layer.mask = maskLayer
backgroundColor = divColor
}
}
Using Defaults:
Result:
and some changes:
Result:
A UIImageView holds a static bitmap image. You could just generate an image like that, save it as a JPEG/PNG/TIF, and load the image into a UIImageView as a bitmap. That doesn't sound like what you want however.
I'd suggest creating a custom subclass of UIView. From there you could go a couple of different ways.
You could have your view override the draw() method for UIView and use Core Graphics calls to draw into the graphics context. Core Graphics is pretty specialized and will require some research to get the hang of.
You could have your custom view add Core Animation (CA) layers that draw your shapes for you. The class CAShapeLayer would be a good choice for this. You'll need to read up on CALayers and how to use them (which is also fairly arcane bit of learning.)
In general Apple steers you towards using layers and letting the system do the rendering for you. That's probably how I would do this. (Using CAShapeLayers, which in turn use CGPath objects.)
I created a custom view based on what you need based on #DuncanC's suggestions
class AngledSplitView: UIView {
var leftLayer: CAShapeLayer!
var rightLayer: CAShapeLayer!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
init (frame: CGRect,
leftColor: UIColor,
rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
super.init(frame: frame)
setupViews(leftColor: leftColor,
rightColor: rightColor,
separatorWidth: separatorWidth,
separatorAngleInDegrees: separatorAngleInDegrees)
}
func setupViews(leftColor: UIColor,
rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
// sets the image's frame to fill our view
createLeftView(leftColor: leftColor, separatorWidth: separatorWidth, separatorAngleInDegrees: separatorAngleInDegrees)
createRightView(rightColor: rightColor, separatorWidth: separatorWidth, separatorAngleInDegrees: separatorAngleInDegrees)
}
func setLeftColor(leftColor: UIColor) {
leftLayer.fillColor = leftColor.cgColor
}
func setRightColor(rightColor: UIColor) {
rightLayer.fillColor = rightColor.cgColor
}
func createLeftView(leftColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
let path = UIBezierPath()
let leftLayer = CAShapeLayer()
let offset = (bounds.height / 2) * tan(separatorAngleInDegrees * CGFloat.pi / 180)
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: bounds.width / 2 - separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to:CGPoint(x: bounds.width / 2 - separatorWidth / 2 - offset,
y: bounds.height))
path.addLine(to:CGPoint(x: bounds.origin.x, y: bounds.height))
path.addLine(to:bounds.origin)
path.close()
leftLayer.path = path.cgPath
leftLayer.fillColor = leftColor.cgColor
self.layer.addSublayer(leftLayer)
}
func createRightView(rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
let path = UIBezierPath()
let rightLayer = CAShapeLayer()
let offset = (bounds.height / 2) * tan(separatorAngleInDegrees * CGFloat.pi / 180)
path.move(to: CGPoint(x: bounds.width / 2 + separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to: CGPoint(x: bounds.width / 2 + separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to:CGPoint(x: bounds.width / 2 + separatorWidth / 2 - offset,
y: bounds.height))
path.addLine(to:CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to:CGPoint(x: bounds.width, y: bounds.origin.y))
path.close()
rightLayer.path = path.cgPath
rightLayer.fillColor = rightColor.cgColor
self.layer.addSublayer(rightLayer)
}
}
You can use it like this:
let customView = AngledSplitView(
frame: CGRect(x: 20, y: 30, width: view.frame.width - 40, height:
view.frame.height / 4),
leftColor: .red,
rightColor: .blue,
separatorWidth: 20,
separatorAngleInDegrees: 45)
view.addSubview(customView)

Animate UISlider's buffer track change?

Following an online article and using a github project I was able to create a UISlider with a second track (bufferTrack).
The only problem I am facing is with updating the burrerEndValue / value. It is not animated. How could I achieve a smooth animation on the UIBezierPath?
open class BufferSlider: UISlider {
open var bufferStartValue:Double = 0 {
didSet{
if bufferStartValue < 0.0 {
bufferStartValue = 0
}
if bufferStartValue > bufferEndValue {
bufferStartValue = bufferEndValue
}
self.setNeedsDisplay()
}
}
open var bufferEndValue:Double = 0 {
didSet{
if bufferEndValue > 1.0 {
bufferEndValue = 1
}
if bufferEndValue < bufferStartValue{
bufferEndValue = bufferStartValue
}
self.setNeedsDisplay()
}
}
open var baseColor:UIColor = UIColor.white
open var progressColor:UIColor?
open var bufferColor:UIColor?
open var customBorderWidth: Double = 0.1{
didSet{
if customBorderWidth < 0.1 {
customBorderWidth = 0.1
}
self.setNeedsDisplay()
}
}
open var sliderHeight: Double = 6 {
didSet{
if sliderHeight < 1 {
sliderHeight = 1
}
}
}
override open func setValue(_ value: Float, animated: Bool) {
super.setValue(value, animated: animated)
self.setNeedsDisplay()
}
override init(frame: CGRect) {
super.init(frame: frame)
updateView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateView()
}
func updateView() {
baseColor = UIColor.white
progressColor = appColors.red
bufferColor = appColors.fadedRed
}
open override func trackRect(forBounds bounds: CGRect) -> CGRect {
var result = super.trackRect(forBounds: bounds)
result.size.height = 0.01
return result
}
open override func draw(_ rect: CGRect) {
baseColor.set()
let rect = self.bounds.insetBy(dx: CGFloat(customBorderWidth), dy: CGFloat(customBorderWidth))
let height = sliderHeight.CGFloatValue
let radius = height/2
let sliderRect = CGRect(x: rect.origin.x, y: rect.origin.y + (rect.height/2-radius), width: rect.width, height: rect.width) //default center
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: sliderRect.minX + radius, y: sliderRect.minY+radius), radius: radius, startAngle: CGFloat(Double.pi)/2, endAngle: -CGFloat(Double.pi)/2, clockwise: true)
path.addLine(to: CGPoint(x: sliderRect.maxX-radius, y: sliderRect.minY))
path.addArc(withCenter: CGPoint(x: sliderRect.maxX-radius, y: sliderRect.minY+radius), radius: radius, startAngle: -CGFloat(Double.pi)/2, endAngle: CGFloat(Double.pi)/2, clockwise: true)
path.addLine(to: CGPoint(x: sliderRect.minX + radius, y: sliderRect.minY+height))
baseColor.setStroke()
path.lineWidth = customBorderWidth.CGFloatValue
path.stroke()
path.fill()
path.addClip()
var fillHeight = sliderRect.size.height-customBorderWidth.CGFloatValue
if fillHeight < 0 {
fillHeight = 0
}
let fillRect = CGRect(
x: sliderRect.origin.x + sliderRect.size.width*CGFloat(bufferStartValue),
y: sliderRect.origin.y + customBorderWidth.CGFloatValue/2,
width: sliderRect.size.width*CGFloat(bufferEndValue-bufferStartValue),
height: fillHeight)
if let color = bufferColor { color.setFill() }
else if let color = self.superview?.tintColor{ color.setFill()}
else{ UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0).setFill() }
UIBezierPath(rect: fillRect).fill()
if let color = progressColor{
color.setFill()
let fillRect = CGRect(
x: sliderRect.origin.x,
y: sliderRect.origin.y + customBorderWidth.CGFloatValue/2,
width: sliderRect.size.width*CGFloat((value-minimumValue)/(maximumValue-minimumValue)),
height: fillHeight)
UIBezierPath(rect: fillRect).fill()
}
}
}
extension Double{
var CGFloatValue: CGFloat {
return CGFloat(self)
}
}
You can't animate the UIView instance when you implemented custom drawing for it in draw(rect:) function, because during animation self.layer.presentationLayer has to be drawn with interpolated values between oldValue and newValue, but your overridden logic of drawing draws always with the newly set value newValue.
You can do custom drawing with animations that you want only in CALayer instance.
Consider implementing of your drawing logic in BufferSliderLayer: CALayer.
For animations on the layer, you'd need to interpolate values that you want to animate, e.g. bufferEndValue and value.
In order to do that, you can refer to this article.
Then, just add BufferSliderLayer onto your BufferSlider view's layer in slider's init(frame:) initialiser and properly size your layer in layoutSubviews.

UIBezierPath partially stroke

I have this code to draw a rectangle which is rounded rect only on one side.
override func draw(_ rect: CGRect) {
// Drawing code
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineWidth = CGFloat(4)
let pathRect = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
let path = UIBezierPath(roundedRect: pathRect.inset(by: UIEdgeInsets(top: lineWidth, left: lineWidth, bottom: lineWidth, right: 0)), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 7, height: 7))
context.setFillColor(UIColor.black.cgColor)
path.fill()
context.setLineWidth(lineWidth)
}
I want to stroke it with red color on all but the right edge (no stroke on the right edge). How do I do it?
You’ll have to create your own path.
A couple of observations:
Don’t use the rect parameter. The rect is what is being asked to being drawn at this point in time, which may not be the entire view. Use bounds when figuring out what the overall path should be.
I might inset the path so that the stroke stays within the bounds of the view.
You can make this #IBDesignable if you want to also be able to see it rendered in IB.
You don’t really need UIGraphicsGetCurrentContext(). The UIKit methods fill(), stroke(), setFill(), and setStroke() methods automatically use the current context.
Thus:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { setNeedsDisplay() } }
#IBInspectable var radius: CGFloat = 7 { didSet { setNeedsDisplay() } }
#IBInspectable var fillColor: UIColor = .black { didSet { setNeedsDisplay() } }
#IBInspectable var strokeColor: UIColor = .red { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.stroke()
}
}
That yields:
Theoretically, it might be more efficient to use CAShapeLayer and let Apple take care of the draw(_:) for us. E.g., they may have optimized the rendering to handle partial view updates, etc.
That might look like the following:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { updatePath() } }
#IBInspectable var radius: CGFloat = 7 { didSet { updatePath() } }
#IBInspectable var fillColor: UIColor = .black { didSet { shapeLayer.fillColor = fillColor.cgColor } }
#IBInspectable var strokeColor: UIColor = .red { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
private extension OpenRightView {
func configure() {
layer.addSublayer(shapeLayer)
}
func updatePath() {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
}
}

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