Draw Rounded Intersect Lines - ios

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.

Related

How to add a custom shape to an UIImageView in Swift?

I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}

Drawing a custom view using UIBezierPath results in a non-symmetrical shape

I'm trying to draw an UIView with some 'curvy edges'.
Here's what it's supposed to look like:
here's what I got:
Notice how the top right (TR) corner is not symmetrical to the bottom right (BR) corner ? The BR corner is very similar to what I want to achieve but I can't get the TR corner to align correctly (played around with bunch of different start and end angles).
here's the code:
struct Constants {
static let cornerRadius: CGFloat = 15.0 // used for left-top and left-bottom curvature
static let rightTipWidth: CGFloat = 40.0 // the max. width for the right tip thingy
static let rightCornerRadius: CGFloat = 10.0 // the radius for the right tip
static let rightEdgeRadius: CGFloat = 10.0 // the radius for the top right and bottom right curvature
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Just a quick summary of my thought process:
Create a bezierPath and move it to the startingPoint
Add the LT (left-top) curve and move the line downards
Move the line along the left edge and add the LB (left-bottom) curve
and the move line along the bottom to the right edge
Move the line till frame.size.width - Constants.rightTipWidth
Add an arc with a center point at x = currentPoint and y = height- rightEdgeRadius
Move the line up until y = (height / 2.0) +
(Constants.rightCornerRadius / 2.0)
Add the QuadCurve with an end point of y = (height / 2.0) -
(Constants.rightCornerRadius / 2.0)
Move the line up till x = maxXRightEdge + Constants.rightEdgeRadius
Add the top right (TR) curve ---> resulting in a non-symmetrical
curvature
Here is another rendition:
#IBDesignable
open class PointerView: UIView {
/// The left-top and left-bottom curvature
#IBInspectable var cornerRadius: CGFloat = 15 { didSet { updatePath() } }
/// The radius for the right tip
#IBInspectable var rightCornerRadius: CGFloat = 10 { didSet { updatePath() } }
/// The radius for the top right and bottom right curvature
#IBInspectable var rightEdgeRadius: CGFloat = 10 { didSet { updatePath() } }
/// The fill color
#IBInspectable var fillColor: UIColor = .blue { didSet { shapeLayer.fillColor = fillColor.cgColor } }
/// The stroke color
#IBInspectable var strokeColor: UIColor = .clear { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
/// The angle of the tip
#IBInspectable var angle: CGFloat = 90 { didSet { updatePath() } }
/// The line width
#IBInspectable var lineWidth: CGFloat = 0 { didSet { updatePath() } }
/// The shape layer for the pointer
private lazy var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.fillColor = fillColor.cgColor
_shapeLayer.strokeColor = strokeColor.cgColor
_shapeLayer.lineWidth = lineWidth
return _shapeLayer
}()
public override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(shapeLayer)
}
open override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let path = UIBezierPath()
let offset = lineWidth / 2
let boundingRect = bounds.insetBy(dx: offset, dy: offset)
let arrowTop = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.minY)
let arrowRight = CGPoint(x: boundingRect.maxX, y: boundingRect.midY)
let arrowBottom = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.maxY)
let start = CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.minY)
// top left
path.move(to: start)
path.addQuadCurve(to: CGPoint(x: boundingRect.minX, y: boundingRect.minY + cornerRadius), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.minY))
// left
path.addLine(to: CGPoint(x: boundingRect.minX, y: boundingRect.maxY - cornerRadius))
// lower left
path.addQuadCurve(to: CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.maxY), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.maxY))
// bottom
path.addLine(to: calculate(from: path.currentPoint, to: arrowBottom, less: rightEdgeRadius))
// bottom right (before tip)
path.addQuadCurve(to: calculate(from: arrowRight, to: arrowBottom, less: rightEdgeRadius), controlPoint: arrowBottom)
// bottom edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowRight, less: rightCornerRadius))
// tip
path.addQuadCurve(to: calculate(from: arrowTop, to: arrowRight, less: rightCornerRadius), controlPoint: arrowRight)
// top edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowTop, less: rightEdgeRadius))
// top right (after tip)
path.addQuadCurve(to: calculate(from: start, to: arrowTop, less: rightEdgeRadius), controlPoint: arrowTop)
path.close()
shapeLayer.lineWidth = lineWidth
shapeLayer.path = path.cgPath
}
/// Calculate some point between `startPoint` and `endPoint`, but `distance` from `endPoint
///
/// - Parameters:
/// - startPoint: The starting point.
/// - endPoint: The ending point.
/// - distance: Distance from the ending point
/// - Returns: Returns the point that is `distance` from the `endPoint` as you travel from `startPoint` to `endPoint`.
private func calculate(from startPoint: CGPoint, to endPoint: CGPoint, less distance: CGFloat) -> CGPoint {
let angle = atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)
let totalDistance = hypot(endPoint.y - startPoint.y, endPoint.x - startPoint.x) - distance
return CGPoint(x: startPoint.x + totalDistance * cos(angle),
y: startPoint.y + totalDistance * sin(angle))
}
}
And because that is #IBDesignable, I can put it in a separate framework target and then optionally use it (and customize it) right in Interface Builder:
The only change I made in parameters was to not use the width of the tip, but rather the angle of the tip. That way, if the size changes as constraints (or whatever) change, it preserves the desired shape.
I also changed this to use a CAShapeLayer rather that a custom draw(_:) method to enjoy any efficiencies that Apple has built in to shape layers.
I don't know your implementation but I think it will be easy if you implemented it like that , that way you cam achieve symmetric shape perfectly
to draw a triangle , just tweak the positions of triangle points
class TriangleView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
context.move(to: CGPoint(x: rect.minX, y: rect.maxY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
context.addLine(to: CGPoint(x: (rect.maxX / 2.0), y: rect.minY))
context.closePath()
context.setFillColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.60)
context.fillPath()
}
}
Here, you forgot halfRadius
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
Full code:
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}

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)

CAShaperLayer as mask show only 1/4 of UIView

I try to make UIView to show zig-zag bottom edge. Something like http://www.shutterstock.com/pic-373176370/stock-vector-receipt-vector-icon-invoice-flat-illustration-cheque-shadow-bill-with-total-cost-amount-and-dollar-symbol-abstract-text-receipt-paper-isolated-on-green.html?src=zMGBKj_5etMCcRB3cKmCoA-1-2
I have method that create a path and set as mask, but it show as 1/4 of the view. Do I need to set something else? Look like a retina problem or coordinate problem, but don't sure which one.
func layoutZigZag(bounds: CGRect) -> CALayer {
let maskLayer = CAShapeLayer()
maskLayer.bounds = bounds
let path = UIBezierPath()
let width = bounds.size.width
let height = bounds.size.height
let topRight = CGPoint(x: width , y: height)
let topLeft = CGPoint(x: 0 , y: height)
let bottomRight = CGPoint(x: width , y: 0)
let bottomLeft = CGPoint(x: 0 , y: 0)
let zigzagHeight: CGFloat = 10
let numberOfZigZag = Int(floor(width / 23.0))
let zigzagWidth = width / CGFloat(numberOfZigZag)
path.move(to: topLeft)
path.addLine(to: bottomLeft)
// zigzag
var currentX = bottomLeft.x
var currentY = bottomLeft.y
for i in 0..<numberOfZigZag {
let upper = CGPoint(x: currentX + zigzagWidth / 2, y: currentY + zigzagHeight)
let lower = CGPoint(x: currentX + zigzagWidth, y: currentY)
path.addLine(to: upper)
path.addLine(to: lower)
currentX += zigzagWidth
}
path.addLine(to: topRight)
path.close()
maskLayer.path = path.cgPath
return maskLayer
}
and
let rect = CGRect(x: 0, y: 0, width: 320, height: 400)
let view = UIView(frame: rect)
view.backgroundColor = UIColor.red
let zigzag = layoutZigZag(bounds: rect)
view.layer.mask = zigzag
Path look correct
Result is 1/4 of the view
Change maskLayer.bounds = bounds to maskLayer.frame = bounds
Update:
Upside down is because of difference between the UI and CG, we are creating the path in UIBezierPath and converting that path as a CGPath (maskLayer.path = path.cgPath). First we have to know the difference, where CGPath is Quartz 2D and origin is at the bottom left while in UIBezierPath is UIKit origin is at the top-left. As per your code, applied coordinates are as per the top-left ie UIBezierPath when we transform to CGPath (origin at bottom left) it becomes upside down. so change the code as below to get the desired effect.
let topRight = CGPoint(x: width , y: 0)
let topLeft = CGPoint(x: 0 , y: 0)
let bottomLeft = CGPoint(x: 0 , y: (height - zigzagHeight))
Quartz 2D Coordinate Systems
UIBezierPath Coordinate Systems

Creating Triangle with UIBezierPath in Swift

I am trying to understand how to create a triangle shape with Swift. I found this code that creates a triangle.
class TriangleLayer: CAShapeLayer {
let innerPadding: CGFloat = 30.0
override init() {
super.init()
fillColor = Colors.red.CGColor
strokeColor = Colors.red.CGColor
lineWidth = 7.0
lineCap = kCALineCapRound
lineJoin = kCALineJoinRound
path = trianglePathSmall.CGPath
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var trianglePathSmall: UIBezierPath {
let trianglePath = UIBezierPath()
trianglePath.moveToPoint(CGPoint(x: 5.0 + innerPadding, y: 95.0)) // #1
trianglePath.addLineToPoint(CGPoint(x: 50.0, y: 12.5 + innerPadding)) // #2
trianglePath.addLineToPoint(CGPoint(x: 95.0 - innerPadding, y: 95.0)) // #3
trianglePath.closePath()
return trianglePath
}
And this code creates a shape like this
in the middle of the screen.
I tried to tweak and play around with it to understand how it works; however, at this point I realised that I got lost with the logic quite a bit. I placed the CGPoints of above triangle on an x-y axis in my head and it seems something like:
#1 x:35, y:95 #3 x:65, y:95
  #2 x:50, y: 42.5
But the triangle is created upside-down if I place the dots on the x-y axis.
What I want to achieve is what the axis tells, and I want to achieve..
. . .
<like this. not this>
. . .
You just have the axes in your head upside down. The coordinate system starts at 0,0 and extends right in X and down in Y.
So your points are really:
#2 x:50, y: 42.5
#1 x:35, y:95 #3 x:65, y:95
to get your desired triangle you'd have something like:
#1 x:35, y:95 #3 x:65, y:95
#2 x:50, y: 147.5
Result triangles
Code in swift5
//TriangleView
extension UIView {
func setRightTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width //you can use triangleView.frame.size.height
let path = CGMutablePath()
path.move(to: CGPoint(x: heightWidth/2, y: 0))
path.addLine(to: CGPoint(x:heightWidth, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth/2, y:heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setLeftTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: heightWidth/2, y: 0))
path.addLine(to: CGPoint(x:0, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth/2, y:heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setUpTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth, y:heightWidth))
path.addLine(to: CGPoint(x:0, y:heightWidth))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setDownTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x:heightWidth/2, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth, y:0))
path.addLine(to: CGPoint(x:0, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
}
Swift 4.*
The easiest way of doing it by using AutoLayout:
Open your Storyboard and drag a UIView in UIViewController, position it and set the size as you wish (that's the place where the triangle will be). Set the view background to be transparent.
Create a new class, you can name it however you want (I named mine TriangleView). This will be the content of that class:
class TriangleView: UIView {
// predefined variables that can be changed
var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.5)
var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.5)
var firstGradientColor: UIColor = UIColor.white
var secondGradientColor: UIColor = UIColor.blue
let gradient: CAGradientLayer = CAGradientLayer()
override func draw(_ rect: CGRect) {
let height = self.layer.frame.size.height
let width = self.layer.frame.size.width
// draw the triangle
let path = UIBezierPath()
path.move(to: CGPoint(x: width / 2, y: 0))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.close()
// draw the triangle 'upside down'
// let path = UIBezierPath()
// path.move(to: CGPoint(x: 0, y: 0))
// path.addLine(to: CGPoint(x: width, y: 0))
// path.addLine(to: CGPoint(x: width / 2, y: height))
// path.close()
// add path to layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = 1.0
// Add the gradient for the view background if needed
gradient.colors = [firstGradientColor.cgColor, secondGradiendColor.cgColor]
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.frame = self.bounds
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
}
Go to your Storyboard, select the UIView and in Identity Inspector write the class name TriangleView
Enjoy your triangle! :)

Resources