Draw shadow only from 3 sides of UIView - ios

I've successfully implemented drawing a shadow around my UIView like this:
block1.layer.masksToBounds = NO;
block1.layer.shadowOffset = CGSizeMake(0, 0);
block1.layer.shadowRadius = 1;
block1.layer.shadowOpacity = 0.7;
What now happens is I have a rectangular UIView and i would like to draw the shadow around it three sides, leaving the bottom side of it without the shadow.
I know that I have to specify the block1.layer.shadowPath by creating a new UIBezierPath but I'm not sure how to do it.
Obviously, setting layer.shadowOffset won't do the trick for me.
Thanks in advance!

I know you say setting layer.shadowOffset won't work for you, but you are allowed to put in negative values so setting it layer.shadowOffset = CGSizeMake(0.0, -2.0) would come close to the effect you're looking for but of course I expect you want it to be even on the three sides.
So here we go with layer.shadowPath!
UIView *block1 = [[UIView alloc] initWithFrame:CGRectMake(32.0, 32.0, 128.0, 128.0)];
[block1 setBackgroundColor:[UIColor orangeColor]];
[self.view addSubview:block1];
block1.layer.masksToBounds = NO;
block1.layer.shadowOffset = CGSizeMake(0, 0);
block1.layer.shadowRadius = 1;
block1.layer.shadowOpacity = 0.7;
UIBezierPath *path = [UIBezierPath bezierPath];
// Start at the Top Left Corner
[path moveToPoint:CGPointMake(0.0, 0.0)];
// Move to the Top Right Corner
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame), 0.0)];
// Move to the Bottom Right Corner
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame), CGRectGetHeight(block1.frame))];
// This is the extra point in the middle :) Its the secret sauce.
[path addLineToPoint:CGPointMake(CGRectGetWidth(block1.frame) / 2.0, CGRectGetHeight(block1.frame) / 2.0)];
// Move to the Bottom Left Corner
[path addLineToPoint:CGPointMake(0.0, CGRectGetHeight(block1.frame))];
// Move to the Close the Path
[path closePath];
block1.layer.shadowPath = path.CGPath;
And to give you an idea of whats going on, here is the actual shadow path you just drew :)
Its possible to just shift that extra middle point before or after the other lines to choose which side will be omitted.

A bit of improvement for other answers, thanks to Ashok R for swift code.
Since we were creating a triangular view in the background of the view with shadow on all sides, and a white triangle on the sides shadow is not needed.
It breaks in case of views with width comparatively larger than height.
A workaround will be to shift the path for the line where shadow is not needed a bit towards that side of view, instead of creating the triangular view Path completely.
I have created an extension for that -
extension UIView {
func addshadow(top: Bool,
left: Bool,
bottom: Bool,
right: Bool,
shadowRadius: CGFloat = 2.0) {
self.layer.masksToBounds = false
self.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
self.layer.shadowRadius = shadowRadius
self.layer.shadowOpacity = 1.0
let path = UIBezierPath()
var x: CGFloat = 0
var y: CGFloat = 0
var viewWidth = self.frame.width
var viewHeight = self.frame.height
// here x, y, viewWidth, and viewHeight can be changed in
// order to play around with the shadow paths.
if (!top) {
y+=(shadowRadius+1)
}
if (!bottom) {
viewHeight-=(shadowRadius+1)
}
if (!left) {
x+=(shadowRadius+1)
}
if (!right) {
viewWidth-=(shadowRadius+1)
}
// selecting top most point
path.move(to: CGPoint(x: x, y: y))
// Move to the Bottom Left Corner, this will cover left edges
/*
|☐
*/
path.addLine(to: CGPoint(x: x, y: viewHeight))
// Move to the Bottom Right Corner, this will cover bottom edge
/*
☐
-
*/
path.addLine(to: CGPoint(x: viewWidth, y: viewHeight))
// Move to the Top Right Corner, this will cover right edge
/*
☐|
*/
path.addLine(to: CGPoint(x: viewWidth, y: y))
// Move back to the initial point, this will cover the top edge
/*
_
☐
*/
path.close()
self.layer.shadowPath = path.cgPath
}
and set the boolean true for whichever side you want the shadow to appear
myView.addshadow(top: false, left: true, bottom: true, right: true, shadowRadius: 2.0)
// shadow radius is optional above and is set as default at 2.0
or
myView.addshadow(top: true, left: true, bottom: true, right: true, shadowRadius: 2.0)
or
myView.addshadow(top: false, left: false, bottom: true, right: true, shadowRadius: 2.0)

Updating Ryan Poolos Answer to Swift 3.0
Thanks to Ryan Poolos
class sampleViewController: UIViewController {
var block1: UIView! = nil
override func viewDidLoad() {
super.viewDidLoad()
block1 = UIView(frame: CGRect(x: 32.0, y: 32.0, width: 128.0, height: 128.0))
block1.backgroundColor = UIColor.orange
self.view.addSubview(block1)
block1.layer.masksToBounds = false
block1.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
block1.layer.shadowRadius = 1.0
block1.layer.shadowOpacity = 0.7
let path = UIBezierPath()
// Start at the Top Left Corner
path.move(to: CGPoint(x: 0.0, y: 0.0))
// Move to the Top Right Corner
path.addLine(to: CGPoint(x: block1.frame.size.width, y: 0.0))
// Move to the Bottom Right Corner
path.addLine(to: CGPoint(x: block1.frame.size.width, y: block1.frame.size.height))
// This is the extra point in the middle :) Its the secret sauce.
path.addLine(to: CGPoint(x: block1.frame.size.width/2.0, y: block1.frame.size.height/2.0))
// Move to the Bottom Left Corner
path.addLine(to: CGPoint(x: 0.0, y: block1.frame.size.height))
path.close()
block1.layer.shadowPath = path.cgPath
}
}
Result:

Try this
extension CALayer {
func applySketchShadow(color: UIColor, alpha: CGFloat, x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat)
{
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2.0
if spread == 0 {
shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
}

Related

How to set correct path for triangle along with stroke in CAShapLayer?

I've to create triangle with some border that fit's into another view. But the problem is, Once I set stroke then the Triangle is bigger than the container. So, I decided to calculate the inner triangle size
After applied border we'll have two triangle like Homothetic Transformation. I just want to calculate the size of the small triangle. So, the border will be fit into the container.
I tried to set it up, but I can't get it done for triangle. Because of the inner triangle center is not in the container center.
Please note that this is first try with equilateral Triangle.
Originally I want solve with same solution with any shape of Polygons.
I attached full demo xcode project at here.
Please help me to solve this problem. Thank you...
ShapeView.swift
import UIKit
class ShapeView: UIView {
var path: CGPath? {
didSet{
setNeedsDisplay(bounds)
}
}
var cornerRadius: CGFloat = .zero
var borderWidth: CGFloat{
set{
shapeLayer.lineWidth = newValue
makeTriangle()
}
get{
return shapeLayer.lineWidth
}
}
var borderColor: UIColor{
set{
shapeLayer.strokeColor = newValue.cgColor
}
get{
return UIColor(cgColor: shapeLayer.strokeColor ?? UIColor.clear.cgColor)
}
}
var background: UIColor{
set{
shapeLayer.fillColor = newValue.cgColor
setNeedsDisplay(bounds)
}
get{
return UIColor(cgColor: shapeLayer.fillColor ?? UIColor.clear.cgColor)
}
}
private lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = .zero
layer.addSublayer(shapeLayer)
return shapeLayer
}()
func makeTriangle(){
// Homothetic Transformation
// https://math.stackexchange.com/questions/3952129/calculate-bounds-of-the-inner-rectangle-of-the-polygon-based-on-its-constant-bo
// https://stackoverflow.com/questions/65315093/cashapelayer-stroke-issue
let lineWidth: CGFloat = borderWidth/2
let size = (frame.width)-lineWidth
let rect = CGRect(x: lineWidth, y: lineWidth , width:size, height: size)
var cornerRadius: CGFloat = self.cornerRadius-lineWidth
cornerRadius = max(cornerRadius, 0)
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth))
path.addLine(to: CGPoint(x: lineWidth, y: rect.height - lineWidth))
path.addLine(to: CGPoint(x: rect.width - lineWidth, y: rect.height - lineWidth))
path.closeSubpath()
self.path = path
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let path = path else{
return
}
shapeLayer.path = path
}
}
Current Out:
Note that the expressions for the coordinates of the bounds of the inner triangle in the Math.SE post that you linked is not very useful here, because that post assumes an equilateral triangle. However, your triangle is not equilateral. Its base has the same length as its height. The view that it is in is a square after all.
After doing some trigonometry, I've found that the top of the inner triangle's bounding rect is insetted by lineWidth divided by sin(atan(0.5)). The bottom is insetted by lineWidth, obviously. And since the inner bounding rect also has to be a square, the left and right insets are both half of the sum of the top and bottom insets.
Here is a GeoGebra screenshot illustrating where sin(atan(0.5)) comes from (I'm rather bad with GeoGebra, so please forgive the mess):
The right half of the outer triangle has a base that is half of its height, so beta = atan(0.5). Then consider the triangle E'EA2, and notice that sin(beta) = lineWidth / EE', and EE' is the top inset that we want.
// makeTriangle
let lineWidth: CGFloat = borderWidth / 2
let top = lineWidth / sin(atan(0.5))
let bottom = lineWidth
let horizontal = (top + bottom) / 2
let rect = bounds.inset(by:
UIEdgeInsets(
top: top,
left: horizontal,
bottom: bottom,
right: horizontal
)
)
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.closeSubpath()()
self.path = path
Now the outer triangle should fit neatly inside the square. But note that by default, CALayer property changes are animated, and the outer triangle could exceed the bounds of the square if you are sliding the slider too quickly, and the animations can't keep up. You can disable the animations by wrapping the code that sets CALayer properties into a CATransaction, and setDisableActions(true). For example in the setter of borderWidth:
set{
CATransaction.begin()
CATransaction.setDisableActions(true)
shapeLayer.lineWidth = newValue
makeTriangle()
CATransaction.commit()
}

how to set boarder for only top, left and right with corner radius with top-left and top-right in UIView?

I am using below methods to add boarder to left , right and top of the view after that I am set the corner radius to view and I won't give appropriate result.
-(void)addLeftBorder:(UIView *)viewName
{
CALayer *leftBorder = [CALayer layer];
leftBorder.backgroundColor = [UIColor redColor].CGColor;
leftBorder.frame = CGRectMake(0,0,1.0,viewName.frame.size.height);
viewName.clipsToBounds = true;
leftBorder.cornerRadius = 25.0;
[viewName.layer addSublayer:leftBorder];
}
-(void)addRightBorder:(UIView *)viewName
{
CALayer *rightBorder = [CALayer layer];
rightBorder.backgroundColor = [UIColor redColor].CGColor;
rightBorder.frame = CGRectMake(viewName.frame.size.width - 1.0,0,1.0,viewName.frame.size.height);
viewName.clipsToBounds = true;
rightBorder.cornerRadius = 25.0;
[viewName.layer addSublayer:rightBorder];
}
-(void)addtopBorder:(UIView *)viewName
{
CALayer *topBorder = [CALayer layer];
topBorder.backgroundColor = [UIColor redColor].CGColor;
topBorder.frame = CGRectMake(0,0,viewName.frame.size.width,1.0);
viewName.clipsToBounds = true;
topBorder.cornerRadius = 25.0;
[viewName.layer addSublayer:topBorder];
}
Here we are setting corner radius to view.
-(void)setupUI{
[self addLeftBorder:self.replyContainerView];
[self addRightBorder:self.replyContainerView];
[self addtopBorder:self.replyContainerView];
self.replyContainerView.layer.cornerRadius = self.comment.bounds.size.height/2;
self.replyContainerView.clipsToBounds = true;
self.replyContainerView.layer.maskedCorners = kCALayerMaxXMinYCorner | kCALayerMinXMinYCorner;
}
For reference purpose , I attached screen shot for above issue.
Help in swift also appreciated.
Here is a demo in swift iOS Playground of possible approach
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
// example view to have top-side rounded borders
let label = UILabel()
label.frame = CGRect(x: 100, y: 200, width: 200, height: 200)
label.textColor = .black
label.backgroundColor = .white
// radius constant
let radius: CGFloat = 25
// initial part of path to have rounded top cornders
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: radius * 2)) // << border height just for demo
path.addLine(to: CGPoint(x: 0, y: radius))
path.addArc(withCenter: CGPoint(x: radius, y: radius), radius: radius, startAngle: -.pi, endAngle: -.pi/2, clockwise: true)
path.addLine(to: CGPoint(x: label.bounds.maxX - radius, y: 0))
path.addArc(withCenter: CGPoint(x: label.bounds.maxX - radius, y: radius), radius: radius, startAngle: -.pi/2.0, endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: label.bounds.maxX, y: radius * 2)) // << border height just for demo
// shape layer to drop rounded corners
var shape = CAShapeLayer()
shape.path = path.cgPath
shape.strokeColor = UIColor.red.cgColor
shape.lineWidth = 2.0
shape.fillColor = UIColor.clear.cgColor
label.layer.addSublayer(shape)
// ... extending path to complete view bounds to mask
path.addLine(to: CGPoint(x: label.bounds.maxX, y: label.bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: label.bounds.maxY))
path.close()
// ... shape to mask target view
shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
label.layer.mask = shape
view.addSubview(label)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I don't think this is possible in the Storyboard. However you can extend UIView to automatically draw the top-left and top-right border and border-colors on init. Then assign it in your storyboard.
#IBDesignable class BottomSheetView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
drawBorder()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
drawBorder()
}
private func drawBorder() {
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: [.topRight, .topLeft],
cornerRadii: CGSize(width: 16, height: 16))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}
}
For drawing borders on one-side, you can have a look here. You can choose which sides, which section of the side and size of the border.
let topBorder: CALayer = CALayer()
topBorder.frame = CGRect(x: 0, y: 0, width: myView.frame.size.width, height: 1)
topBorder.backgroundColor = UIColor.purple.cgColor
myView.layer.addSublayer(topBorder)

Draw Rounded Intersect Lines

I have to create a tab with buttons and some custom border (Images below). Problem is I have to add cornerRadius for each intersect but I'm not sure how to do it properly.
I have this code to draw tabs with border:
private func drawBorder(selectedTab: UIButton) {
// The Tab frame (below one)
guard let tabContainerFrame = vTabContainer?.frame else { return }
let borderColor = selectedTab.titleColor(for: .selected)
let tabFrame = selectedTab.convert(selectedTab.bounds, to: self)
let topMargin: CGFloat = 5
let tabOrigin = CGPoint(x: tabFrame.origin.x, y: tabFrame.origin.y - topMargin)
// Make paths to draw
let path = UIBezierPath()
path.move(to: tabOrigin) // Origin (top left)
path.addLine(to: CGPoint(x: tabFrame.maxX, y: tabOrigin.y)) // -> right
path.addLine(to: CGPoint(x: tabFrame.maxX, y: tabFrame.maxY)) // -> down
if tabFrame.maxX != tabContainerFrame.maxX {
path.addLine(to: CGPoint(x: tabContainerFrame.maxX, y: tabContainerFrame.origin.y)) // -> right
}
path.addLine(to: CGPoint(x: tabContainerFrame.maxX, y: tabContainerFrame.maxY)) // -> Down
path.addLine(to: CGPoint(x: tabContainerFrame.origin.x, y: tabContainerFrame.maxY)) // -> left
path.addLine(to: CGPoint(x: tabContainerFrame.origin.x, y: tabContainerFrame.origin.y)) // -> up
if tabOrigin.x != tabContainerFrame.origin.x {
path.addLine(to: CGPoint(x: tabOrigin.x, y: tabContainerFrame.origin.y)) // -> right
}
// Close the path. This will create the last line automatically.
path.close()
// Draw
let borderLayer = CAShapeLayer()
borderLayer.path = path.cgPath
borderLayer.lineCap = kCALineCapRound
borderLayer.lineJoin = kCALineJoinBevel
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderColor?.cgColor
borderLayer.cornerRadius = 10
borderLayer.lineWidth = 2
layer.addSublayer(borderLayer)
self.borderLayer = borderLayer
}
This is the result:
As you can see, even though I add cornerRadius = 10, it just doesn't work. borderLayer.lineCap = kCALineCapRound and borderLayer.lineJoin = kCALineJoinBevel doesn't help also.
Bonus:
I'd like to have a way to implement dynamic #IBInspectable var lineCornerRadius: CGFloat = 10.
If you are using UIBezierPath to draw a shape, setting cornerRadius will have no effect on that path.
Instead, you want to use path.addCurve(to: ...) to make your rounded corners.
For example:
the green dotted line is tabFrame
pt1 is tabFrame's "left" and "top + 10" (your radius)
pt2 is tabFrame's "left + 10" and "top"
pt3 is the first curve's second "control point" - the top-left corner of tabFrame
pt4 is tabFrame's "right - 10" and "top"
pt5 is tabFrame's "right" and "top + 10"
pt6 is the second curve's second "control point" - the top-right corner of tabFrame
So
path.addCurve(to: pt2, controlPoint1: pt1, controlPoint2: pt3)
adds a curve to pt2 ... from pt1 ... with curve control point of pt3
then:
path.addLine(to: pt4)
adds a line from the current point (pt2) to pt4
then:
path.addCurve(to: pt5, controlPoint1: pt4, controlPoint2: pt6)
adds a curve to pt5 ... from pt4 ... with curve control point of pt6
The rest of your shape is normal line segments.

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

UIBezierPath Rotation around a UIView's center

I'm creating a custom UIView, in which I implement its draw(rect:) method by drawing a circle with a large width using UIBezierPath, that draw a square on the top (as shown in the picture, don't consider the colors or the size). Then I try creating rotated copies of the square, to match a "settings" icon (picture 2, consider only the outer ring). To do that last thing, I need to rotate the square using a CGAffineTransform(rotationAngle:) but the problem is that this rotation's center is the origin of the frame, and not the center of the circle. How can I create a rotation around a certain point in my view?
As a demonstration of #DuncanC's answer (up voted), here is the drawing of a gear using CGAffineTransforms to rotate the gear tooth around the center of the circle:
class Gear: UIView {
var lineWidth: CGFloat = 16
let boxWidth: CGFloat = 20
let toothAngle: CGFloat = 45
override func draw(_ rect: CGRect) {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 4.0
var path = UIBezierPath()
path.lineWidth = lineWidth
UIColor.white.set()
// Use the center of the bounds not the center of the frame to ensure
// this draws correctly no matter the location of the view
// (thanks #dulgan for pointing this out)
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
// Draw circle
path.move(to: CGPoint(x: center.x + radius, y: center.y))
path.addArc(withCenter: CGPoint(x: center.x, y: center.y), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
path.stroke()
// Box for gear tooth
path = UIBezierPath()
let point = CGPoint(x: center.x - boxWidth / 2.0, y: center.y - radius)
path.move(to: point)
path.addLine(to: CGPoint(x: point.x, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y))
path.close()
UIColor.red.set()
// Draw a tooth every toothAngle degrees
for _ in stride(from: toothAngle, through: 360, by: toothAngle) {
// Move origin to center of the circle
path.apply(CGAffineTransform(translationX: -center.x, y: -center.y))
// Rotate
path.apply(CGAffineTransform(rotationAngle: toothAngle * .pi / 180))
// Move origin back to original location
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
// Draw the tooth
path.fill()
}
}
}
let view = Gear(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
Here it is running in a Playground:
Shift the origin of your transform,
Rotate,
Shift back
Apply your transform
Maybe would help someone. Source
extension UIBezierPath {
func rotate(degree: CGFloat) {
let bounds: CGRect = self.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = degree / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
}
Example:
let progressLayerPath = UIBezierPath(ovalIn: CGRect(x: 0,
y: 0,
width: 70,
height: 70))
progressLayerPath.rotate(degree: -90) // <-------
progressLayer.path = progressLayerPath.cgPath
progressLayer.strokeColor = progressColor.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = lineWidth
layer.addSublayer(progressLayer)
If you want a quick n dirty version using UIViews instead:
UIView * dialView = [UIView new];
dialView.frame = CGRectMake(0, localY, w, 300);
dialView.backgroundColor = [UIColor whiteColor];
[analyticsView addSubview:dialView];
float lineWidth = 6;
float lineHeight = 20.0f;
int numberOfLines = 36;
for (int n = 0; n < numberOfLines; n++){
UIView * lineView = [UIView new];
lineView.frame = CGRectMake((w-lineWidth)/2, (300-lineHeight)/2, lineHeight, lineWidth);
lineView.backgroundColor = [UIColor redColor];
[dialView addSubview:lineView];
lineView.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(360/numberOfLines*n));
lineView.transform = CGAffineTransformTranslate(lineView.transform, (150-lineHeight), 0);
}
Giving:
Where w is a holder width, and radians are calculated by:
#define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)

Resources