Draw a semi-circle button iOS - ios

I am trying to draw a semi-circle button. I'm having trouble both drawing the semi-circle and attaching it to the button I made in xcode. The button in xcode has constraints pinning it to the bottom of the screen and centering it. I reference the button in my view controller and then try to override it to a semi-circle as follows. I am getting a blank button. I also hard coded in the coordinates from the storyboard of where the button is. Is there a better way to do that?
let circlePath = UIBezierPath.init(arcCenter: CGPoint(x: 113.0, y: 434.0),
radius: 10.0, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
sunButton.layer.mask = circleShape

The problem is that your shape layer is positioned way outside the buttons bounds. If you just add the shape layer as a subview of your button's layer you'll see the problem:
button.layer.addSublayer(circleShape)
You have to adjust the arc center point, so that the arc lies within the button's bounds:
let circlePath = UIBezierPath.init(arcCenter: CGPointMake(button.bounds.size.width / 2, 0), radius: button.bounds.size.height, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
button.layer.mask = circleShape
And you'll get this:

Swift 5 update:
let circlePath = UIBezierPath.init(arcCenter: CGPoint(x: button.bounds.size.width / 2, y: 0), radius: button.bounds.size.height, startAngle: 0.0, endAngle: .pi, clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
button.layer.mask = circleShape

Swift 5:
// semiCircleDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width/2, y: 0), radius: shapeView.bounds.size.height, startAngle: 0, endAngle: .pi, clockwise: true).cgPath
//semiCircleUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width/2, y: 0), radius: shapeView.bounds.size.height, startAngle: 2 * .pi, endAngle: .pi, clockwise: true).cgPath
//quarterCircleRightDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: shapeView.bounds.size.width, startAngle: 2 * .pi, endAngle: .pi * 3/2, clockwise: true).cgPath
//quarterCircleLeftDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width, y: 0), radius: shapeView.bounds.size.width, startAngle: .pi * 3/2, endAngle: .pi, clockwise: true).cgPath
quarterCircleRightUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: shapeView.bounds.size.height), radius: shapeView.bounds.size.width, startAngle: 0, endAngle: .pi/2, clockwise: false).cgPath
//quarterCircleLeftUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width, y: shapeView.bounds.size.height), radius: shapeView.bounds.size.width, startAngle: .pi/2, endAngle: .pi, clockwise: false).cgPath
let shape = CAShapeLayer()
shape.path = circlePath.cgPath
shapeView.layer.mask = shape

Related

Circular Bar Graph

One of the app I'm using has bar graph in circular shape. I've tried to create this by using UIBezierPath with arcCenter but I felt like this is completely wrong approach.
The code I'm currently using:
func drawGraph(){
var startDegrees = 120
for _ in 0...10{
let startAngle: CGFloat = radians(of: startDegrees)
let endAngle: CGFloat = radians(of: startDegrees + 5)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - CGFloat(Int.random(in: 10...50))
let trackPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackPath.lineWidth = trackBorderWidth
trackPath.lineCapStyle = .square
trackBackgroundColor.set()
trackPath.stroke()
startDegrees += 7
}
}
Do you have better solutions or do you know any similar graph libraries like this?
EDIT:
So I've been trying to change my code little bit, I'm able to create something similar with this code:
func drawGraph(){
var temp:CGFloat = 120.0
for i in 0...64{
let startAngle: CGFloat = radians(of: temp)
let endAngle: CGFloat = radians(of: temp + 5)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - 10
let randomNumber = CGFloat(Int.random(in: 10...20))
let trackPath = UIBezierPath(arcCenter: center, radius: radius + randomNumber / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackPath.lineWidth = randomNumber
trackPath.lineCapStyle = .butt
UIColor.randomColor().set()
trackPath.stroke()
temp += 5.5
}
let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: 101, 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
self.layer.addSublayer(shapeLayer)
}

Set the radius of one corner of a SKShapeNode

I want to change the radius of two corners of a SKShapeNode (rect) but I didn't find a working solution.
I've tried to use a path, and it didn't work.
Swift 4.2, iOS 12.1.1, Xcode 10.1
let shape = SKShapeNode()
shape.path = UIBezierPath(roundedRect: CGRect(x: -128, y: -128, width: 256, height: 256), cornerRadius: 64).cgPath
shape.position = CGPoint(x: frame.midX, y: frame.midY)
shape.fillColor = UIColor.red
shape.strokeColor = UIColor.blue
shape.lineWidth = 10
addChild(shape)
I made a function that will allow you to customize each corner radius to whatever size you'd like. You can have 1,2,3 or 4 corners with a radius. If you always just want two corners then I would suggest making a wrapper function so you don't have so many parameters to fill in each time you call it.
func CustomRoundRectPath(_ rect:CGRect, _ TLR:CGFloat,_ TRR:CGFloat,_ BLR:CGFloat,_ BRR:CGFloat) -> CGPath {
let w = rect.width
let h = rect.height
//TLP:(TLP)
let TLP = CGPoint(x: TLR, y: h - TLR)
let TRP = CGPoint(x: w - TRR, y: h - TRR)
let BLP = CGPoint(x: BLR, y: BLR)
let BRP = CGPoint(x: w - BRR, y: BRR)
//Create path and addComponents
let path = CGMutablePath()
path.addArc(center: TLP, radius: TLR, startAngle: CGFloat.pi, endAngle: CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: TRP.x, y: h))
path.addArc(center: TRP, radius: TRR, startAngle: CGFloat.pi/2, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: w, y: BRP.y))
path.addArc(center: BRP, radius: BRR, startAngle: 0, endAngle: -CGFloat.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: BLP.x, y: 0))
path.addArc(center: BLP, radius: BLR, startAngle: -CGFloat.pi/2, endAngle: -CGFloat.pi, clockwise: true)
path.addLine(to: CGPoint(x: 0, y: TLP.y))
return path
}

How to shift arc center in SpriteKit?

I have one circle and 4 arcs located around that circle.
Currently I'm drawing arcs like this:
fileprivate func addArc(from startAngle: CGFloat, to endAngle: CGFloat, of color: SKColor, with text: String) {
let arc = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 100, startAngle: startAngle, endAngle: endAngle, clockwise: false)
arc.flatness = 100
let node = SKShapeNode()
node.lineWidth = 36
node.strokeColor = color
node.path = arc.cgPath
addChild(node)
}
and when I later try to add a new LabelNode into my arc,
let labelNode = SKLabelNode(text: text)
labelNode.horizontalAlignmentMode = .center
labelNode.verticalAlignmentMode = .center
node.addChild(labelNode)
that NodeLabel being added into the middle of the circle but not into the middle of the arcs.
I've found that the mistake is in this line:
let arc = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 100, startAngle: startAngle, endAngle: endAngle, clockwise: false)
where I'm setting arcCenter.
So the question is, how can I shift the center of those arcs so labelNodes will be added into arcs but not into the circle?

Swift: UIBezierPath Stroke Animation from Center

I've been making a simple UIBezierPath animation with Swift. This path consists on creating a rounded rectangle with a colored border. The animation must be the drawing of the colored border. To do so, I've created a CAShapeLayer with a UIBezierPath(roundedRect:, cornerRadius: )
let layer = CAShapeLayer()
var viewPrueba = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
viewPrueba = UIView(frame: CGRectMake(self.view.frame.width/2-100, self.view.frame.height/2 - 100, 200, 200))
self.view.addSubview(viewPrueba)
let path = UIBezierPath(roundedRect: CGRectMake(0, 0, 200, 200), cornerRadius: 40.0)
layer.path = path.CGPath
layer.fillColor = UIColor.clearColor().CGColor
layer.strokeColor = UIColor.blueColor().CGColor
layer.strokeStart = 0.0
layer.strokeEnd = 0.0
layer.lineWidth = 4.0
layer.lineJoin = kCALineJoinRound
viewPrueba.layer.addSublayer(layer)
let tapGR = UITapGestureRecognizer(target: self, action: #selector(ViewController.anim))
self.view.addGestureRecognizer(tapGR)
}
func anim() {
let anim1 = CABasicAnimation(keyPath: "strokeEnd")
anim1.fromValue = 0.0
anim1.toValue = 1.0
anim1.duration = 4.0
anim1.repeatCount = 0
anim1.autoreverses = false
anim1.removedOnCompletion = false
anim1.additive = true
anim1.fillMode = kCAFillModeForwards
self.layer.addAnimation(anim1, forKey: "strokeEnd")
}`
It works well. The only problem is that the animation starts from the top-left part of the square and not from the top-center. How can I do that?
The only thing I've found in order to achieve this is by doing it with a circle and not a rectangle, which is not what we want.
Thanks
CoreAnimate animated as the same order as which the UIBezierPath was drawn.
The system method
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
return a UIBezierPath which was drawn from the top-left,so your animation started from the top-left.
But you can create your own UIBezierPath drawn form top-center:
func centerStartBezierPath(frame:CGRect,cornerRadius:CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.moveToPoint(CGPointMake(frame.width/2.0, 0))
path.addLineToPoint(CGPointMake(frame.width-cornerRadius, 0))
path.addArcWithCenter(CGPointMake(frame.width-cornerRadius, cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(-M_PI/2),
endAngle: 0,
clockwise: true)
path.addLineToPoint(CGPointMake(frame.width, frame.height-cornerRadius))
path.addArcWithCenter(CGPointMake(frame.width-cornerRadius, frame.height-cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: CGFloat(M_PI/2),
clockwise: true)
path.addLineToPoint(CGPointMake(cornerRadius, frame.height))
path.addArcWithCenter(CGPointMake(cornerRadius, frame.height-cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(M_PI/2),
endAngle: CGFloat(M_PI),
clockwise: true)
path.addLineToPoint(CGPointMake(0, cornerRadius))
path.addArcWithCenter(CGPointMake(cornerRadius, cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(M_PI),
endAngle: CGFloat(M_PI*3/2),
clockwise: true)
path.closePath()
path.applyTransform(CGAffineTransformMakeTranslation(frame.origin.x, frame.origin.y))
return path;
}
And it works like this:
You can also change the code,and start from any point you want.
Swift syntax changed much during these three years. Here is my updated version of originally accepted answer, but in Swift 5.1+
private extension UIBezierPath {
convenience init(roundedRectFromCenter frame: CGRect, cornerRadius: CGFloat) {
self.init()
move(to: CGPoint(x: frame.width / 2, y: 0))
addLine(to: CGPoint(x: frame.width - cornerRadius, y: 0))
addArc(
withCenter: CGPoint(x: frame.width - cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: -.pi / 2,
endAngle: 0,
clockwise: true
)
addLine(to: CGPoint(x: frame.width, y: frame.height - cornerRadius))
addArc(
withCenter: CGPoint(x: frame.width - cornerRadius, y: frame.height - cornerRadius),
radius: cornerRadius,
startAngle: 0,
endAngle: .pi / 2,
clockwise: true
)
addLine(to: CGPoint(x: cornerRadius, y: frame.height))
addArc(
withCenter: CGPoint(x: cornerRadius, y: frame.height - cornerRadius),
radius: cornerRadius,
startAngle: .pi / 2,
endAngle: .pi,
clockwise: true
)
addLine(to: CGPoint(x: 0, y: cornerRadius))
addArc(
withCenter: CGPoint(x: cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi * 3 / 2,
clockwise: true
)
close()
apply(CGAffineTransform(
translationX: frame.origin.x,
y: frame.origin.y
))
}
}

Swift: UIBezierPath bezierPathByReversingPath() Issues

I am using bezierPathByReversingPath() trying to make an animating circle. I finally found a method that will work, but I am getting this bar across the circle that is not being "deleted".
The Code:
#IBDesignable class circle: UIView {
override func drawRect(rect: CGRect) {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(250), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(200), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true)
circlePath.appendPath(circlePath2.bezierPathByReversingPath())
circlePath.fill() //appendPath(circlePath.bezierPathByReversingPath())
}
}
The Picture
P.S. If you want an example of what I am doing, it is a win/lose animation in the iOS game "Dulp".
It is because UIBezierPath(arcCenter: center, radius: CGFloat(250), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true) takes radians, not degrees, as you specify here.
Thus, one would need to supply the arguments in radians.
I created a simple function that converts from degrees to radians; the result turned out fine.
Here is the function:
func degreesToRadians(degree : Int) -> CGFloat
{
return CGFloat(degree) * CGFloat(M_PI) / 180.0
}
Usage:
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius: 250.0, startAngle: self.degreesToRadians(0), endAngle:
self.degreesToRadians(360), clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius: 200.0, startAngle: self.degreesToRadians(0), endAngle:
self.degreesToRadians(360), clockwise: true)
Or one could create a protocol extension:
extension Double
{
var degreesToRad : CGFloat
{
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Usage:
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius: 250.0, startAngle: 0.0.degreesToRad , endAngle: 360.0.degreesToRad, clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius: 200.0, startAngle: 0.0.degreesToRad, endAngle: 360.0.degreesToRad, clockwise: true)
First of all, the angles are in radians - not degrees. So your circles are going around 30 or so times and not ending up where they started. That's what gives you the bar.
Secondly, you can just stroke one circle instead of creating two and filling them:
#IBDesignable class Circle: UIView {
override func drawRect(rect: CGRect) {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(225), startAngle: CGFloat(0), endAngle: CGFloat(2*M_PI), clockwise: true)
circlePath.lineWidth = 50
circlePath.stroke()
}
}

Resources