Create Custom shaped buttons in swift - ios

I am new to swift and not sure how to work around with this.
I wanted to design a custom view which I want to have buttons in the shape of an circle.(Screenshot attached)
I was able to do it by using layers and adding the layers using the following :
// radius is the half the frame's width or height (whichever is smallest)
let radius = min(frame.size.width, frame.size.height) * 0.5
// center of the view
let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)
// enumerate the total value of the segments by using reduce to sum them
var valueCount = outerSegments.reduce(0, {$0 + $1.value})
var startAngle : CGFloat = 0
var i = 0
// var data: [String] = ["","","","","","","","","","","",""]
for segment in outerSegments { // loop through the values array
let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath(arcCenter: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = segment.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.borderWidth = 1.0
shape.borderColor = UIColor.black.cgColor
shape.name = "\(i)"
datamap[shape.name ?? ""] = data[i]
//addText(data[i], to: shape)
layer.addSublayer(shape)
startAngle = endAngle
i += 1
}
valueCount = innerSegments.reduce(0, {$0 + $1.value})
startAngle = 0
i = 100
for segment in innerSegments {
let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath(arcCenter: viewCenter, radius: 2*radius/3, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = segment.color.cgColor
shape.borderWidth = 1.0
shape.borderColor = UIColor.black.cgColor
shape.name = "\(i)"
//datamap[shape.name ?? ""] = data[i-100]
//addText(data[i-100], to: shape)
layer.addSublayer(shape)
startAngle = endAngle
i += 1
}
valueCount = innerMostSegments.reduce(0, {$0 + $1.value})
startAngle = 0
i = 1000
for segment in innerMostSegments {
let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath(arcCenter: viewCenter, radius: radius/3, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = segment.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.borderWidth = 1.0
shape.borderColor = UIColor.black.cgColor
shape.name = "\(i)"
datamap[shape.name ?? ""] = data[i-1000]
//addText(data[i-1000], to: shape)
layer.addSublayer(shape)
startAngle = endAngle
i += 1
}
This works perfectly fine but I want to work with the same shape but instead of using the layer, I want to define Buttons instead of the layers in the view. If I try to add the UIButton directly, the screen isn't tappable anymore.
Can anyone advise with the same.
Thanks
EDIT - I tried to add button using the following strategy :
for segment in innerSegments {
let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
let shape = CAShapeLayer()
let button = UIButton(frame: accessibilityFrame)
let path: UIBezierPath = UIBezierPath(arcCenter: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = segment.color.cgColor
shape.strokeColor = UIColor.black.cgColor
shape.borderWidth = 1.0
shape.borderColor = UIColor.black.cgColor
shape.name = "\(i)"
datamap[shape.name ?? ""] = data[i]
//addText(data[i], to: shape)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.layer.addSublayer(shape)
layer.addSublayer(button.layer)
startAngle = endAngle
i += 1
.
.
.

Related

Add Shadow to CAShapeLayer with UIBezierPath

need some help adding a drop shadow to a line created with a CAShapeLayer / UIBezierPath
This code snippet produces the following line shape as in the screenshot below
let bounds = UIScreen.main.bounds
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 10.0
shapeLayer.frame = CGRect(x: 0, y: 0 , width: bounds.width, height: bounds.width)
shapeLayer.lineWidth = 5.0
shapeLayer.strokeColor = UIColor.red.cgColor
let offset : CGFloat = 20
let arcCenter = shapeLayer.position
let radius = shapeLayer.bounds.size.width / 2.0 - offset
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(1.0 * .pi)
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowColor = UIColor.red.cgColor
shapeLayer.shadowOpacity = 1.0
shapeLayer.shadowRadius = 25
shapeLayer.path = circlePath.cgPath
As you can see the shadow surrounds the outer part of the semi-circle shape.
Could anyone give me any hints on adding a drop shadow around the line only ?
Output:
UIView Extension:
extension UIView {
func renderCircle() {
let semiCircleLayer = CAShapeLayer()
let center = CGPoint (x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let circleRadius = self.frame.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(Double.pi * 2), endAngle: CGFloat(Double.pi), clockwise: true)
semiCircleLayer.path = circlePath.cgPath
semiCircleLayer.strokeColor = UIColor.red.cgColor
semiCircleLayer.fillColor = UIColor.clear.cgColor
semiCircleLayer.lineWidth = 8
semiCircleLayer.shadowColor = UIColor.red.cgColor
semiCircleLayer.shadowRadius = 25.0
semiCircleLayer.shadowOpacity = 1.0
semiCircleLayer.shadowPath = circlePath.cgPath.copy(strokingWithWidth: 25, lineCap: .round, lineJoin: .miter, miterLimit: 0)
semiCircleLayer.shadowOffset = CGSize(width: 1.0, height: 1.0)
self.layer.addSublayer(semiCircleLayer)
}
}
Usage:
semiCircleView.renderCircle()

How to draw Semi(half) circle and animate it till particular angle in swift 4?

let slayer = CAShapeLayer()
let center = CGPoint(x: (bounds.width / 2) + 4, y: bounds.height - 8)
let radius: CGFloat = bounds.height - 16
let startAngle: CGFloat = 4 * .pi / 4
let endAngle: CGFloat = 0.0
slayer.path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true).cgPath
slayer.lineWidth = 15.0
slayer.lineCap = kCALineCapRound
slayer.strokeColor = UIColor.blue.cgColor
slayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(slayer)
Please look at the following image. The requirement is this circle with animation it till a particular angle.
You can animate the circle by adding another layer on top of it and animating it's stroke end property.
func animate()
{
let slayer = CAShapeLayer()
let center = CGPoint(x: (bounds.width / 2) + 4, y: bounds.height - 8)
let radius: CGFloat = bounds.height - 16
let startAngle: CGFloat = 4 * .pi / 4
let endAngle: CGFloat = 0.0
slayer.path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true).cgPath
slayer.lineWidth = 15.0
slayer.lineCap = kCALineCapRound
slayer.strokeColor = UIColor.blue.cgColor
slayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(slayer)
slayer.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 60.0 . //Customize the time of your animation here.
animation.fromValue = 0.0
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name:
kCAMediaTimingFunctionLinear)
slayer.strokeEnd = 1.0
slayer.add(animation, forKey: nil)
}
Use 2 CA Shape Layer objects, one for gray(w/o animation) & other blue(with animation).
func addCircle(color: UIColor) -> CAShapeLayer
{
let centre = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2)
let beizerPath = UIBezierPath(arcCenter: centre, radius: 30.0, startAngle: .pi, endAngle: 2.0 * .pi, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = beizerPath.cgPath
circleLayer.fillColor = color.cgColor
circleLayer.strokeEnd = 1.0
captureButton.layer.addSublayer(circleLayer)
return circleLayer
}
func animateButton()
{
blueLayer = addCircle(color: .blue)
blueLayer!.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 60.0
animation.fromValue = 0.0
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
blueLayer!.strokeEnd = 1.0
blueLayer!.add(animation, forKey: nil)
}

UIImage at last point of UIBezierPath

I want to add a UIImage at last point of UIBezierPath like the image bellow:
Circle Path
Code:
let consume = 0.25
let x = self.frame.size.width / 2.0
let y = self.frame.size.height / 2.0
let radius = (frame.size.width - 10) / 2.0
let startAngle: CGFloat = CGFloat(M_PI * (-1/2))
let endAngle = CGFloat(M_PI * 2 - M_PI/2 * consume)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: x, y: y), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.lineWidth = 8.0;
let image = UIImage(named: "myCoolImage")
let imageView = UIImageView(image: image)
// coordX and coordY must be at last point of UIBezierPath
imageView.frame = CGRect(x: coordX, y: coordY, width: 20.0, height: 20.0)
self.addSubview(imageView)
I've tried the following:
let coordX: CGFloat = cos(endAngle) * radius
let coordY: CGFloat = sin(endAngle) * radius
Result:
Positive coordX and coordY
The main problem is how to calculate the coordX and coordY.
For the sake of having an actual, fully fleshed out answer, here's basically what you need.
let consume = 0.25
let x = self.frame.size.width / 2.0
let y = self.frame.size.height / 2.0
let radius = (frame.size.width - 10) / 2.0
let startAngle: CGFloat = CGFloat(M_PI * (-1/2))
let endAngle = CGFloat(M_PI * 2 - M_PI/2 * consume)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: x, y: y), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.lineWidth = 8.0;
let imageSize = 20.0
// Center + cos/sin(angle) * radius, then subtract half
// the image size so it will be centered on the line
let coordX: CGFloat = x + (cos(endAngle) * radius) - (imageSize / 2.0)
let coordY: CGFloat = y - (sin(endAngle) * radius) - (imageSize / 2.0)
let image = UIImage(named: "myCoolImage")
let imageView = UIImageView(image: image)
// coordX and coordY must be at last point of UIBezierPath
imageView.frame = CGRect(x: coordX, y: coordY, width: imageSize, height: imageSize)
self.addSubview(imageView)

iOS: Draw circle as percentage in UIView

I have one UIView in circular shape, I need to show that UIView border colour in percentage value like if percentage value is 50%, it should fill half border colour of UIView. I have used UIBeizer path addArcWithCenter however I didn't get perfect solution. Please help me in this
You can achieve it with following code, simply adjust strokeStart and strokeEnd:
// round view
let roundView = UIView(frame: CGRectMake(100, 100, 250, 250))
roundView.backgroundColor = UIColor.whiteColor()
roundView.layer.cornerRadius = roundView.frame.size.width / 2
// bezier path
let circlePath = UIBezierPath(arcCenter: CGPoint (x: roundView.frame.size.width / 2, y: roundView.frame.size.height / 2),
radius: roundView.frame.size.width / 2,
startAngle: CGFloat(-0.5 * M_PI),
endAngle: CGFloat(1.5 * M_PI),
clockwise: true)
// circle shape
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
circleShape.strokeColor = UIColor.redColor().CGColor
circleShape.fillColor = UIColor.clearColor().CGColor
circleShape.lineWidth = 1.5
// set start and end values
circleShape.strokeStart = 0.0
circleShape.strokeEnd = 0.8
// add sublayer
roundView.layer.addSublayer(circleShape)
// add subview
self.view.addSubview(roundView)
I've written my custom function for doing this in Swift 5. I'm sure I'll save someone a lot of time. Have fun with it.
func buildRoundView(roundView: UIView, total : Int, current : Int){
roundView.layer.cornerRadius = roundView.frame.size.width / 2
roundView.backgroundColor = .clear
let width :CGFloat = 10.0
let reducer :CGFloat = 0.010
let circlePath = UIBezierPath(arcCenter: CGPoint (x: roundView.frame.size.width / 2, y: roundView.frame.size.height / 2),
radius: roundView.frame.size.width / 2,
startAngle: CGFloat(-0.5 * Double.pi),
endAngle: CGFloat(1.5 * Double.pi),
clockwise: true)
let multiplier = CGFloat((100.000 / Double(total)) * 0.0100)
for i in 1...total {
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
if i <= current {
circleShape.strokeColor = UIColor.systemRed.cgColor
}
else{
circleShape.strokeColor = UIColor.lightGray.cgColor
}
circleShape.fillColor = UIColor.clear.cgColor
circleShape.lineWidth = width
circleShape.strokeStart = CGFloat(CGFloat(i - 1) * multiplier) + reducer
circleShape.strokeEnd = CGFloat(CGFloat(i) * multiplier) - reducer
roundView.layer.addSublayer(circleShape)
}
}
According #gvuksic's answer:
Swift 5:
// round view
let roundView = UIView(
frame: CGRect(
x: circleContainerView.bounds.origin.x,
y: circleContainerView.bounds.origin.y,
width: circleContainerView.bounds.size.width - 4,
height: circleContainerView.bounds.size.height - 4
)
)
roundView.backgroundColor = .white
roundView.layer.cornerRadius = roundView.frame.size.width / 2
// bezier path
let circlePath = UIBezierPath(arcCenter: CGPoint (x: roundView.frame.size.width / 2, y: roundView.frame.size.height / 2),
radius: roundView.frame.size.width / 2,
startAngle: CGFloat(-0.5 * .pi),
endAngle: CGFloat(1.5 * .pi),
clockwise: true)
// circle shape
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.strokeColor = UIColor.customColor?.cgColor
circleShape.fillColor = UIColor.clear.cgColor
circleShape.lineWidth = 4
// set start and end values
circleShape.strokeStart = 0.0
circleShape.strokeEnd = 0.8
// add sublayer
roundView.layer.addSublayer(circleShape)
// add subview
circleContainerView.addSubview(roundView)
And the result:

How to create rounded corners with CAShapeLayer

Is there a way to add rounded corners to a CAShapeLayer? In my case I needed the shape layer to create a dashed border via lineDashPattern.
^ notice how the dashed line is not rounded
The answer is simple. Create a bézier path with rounded corners.
UPDATE for Swift
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [15.0]
view.layer.addSublayer(border)
Objective-C
// (This old code assumes this is within a view with a custom property "border".)
self.clipsToBounds = YES;
self.layer.cornerRadius = 10.0;
self.border = [CAShapeLayer layer];
self.border.fillColor = nil;
self.border.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10.0].cgPath;
self.border.frame = self.bounds;
self.border.strokeColor = [UIColor purpleColor].CGColor;
self.border.lineWidth = borderWidth * 2; // double desired width as half will be clipped
self.border.lineDashPattern = #[#15];
[self.layer addSublayer:self.border];
In swift 4 I created a UIView category (UIView+Borders) 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;
}

Resources