I am trying to draw a line with a smooth quadratic curve attached to its end. Therefore, I am using this code:
import Foundation
import UIKit
class IndicatingView: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
}
func createLineWithCurve() {
path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: self.frame.height - self.frame.width))
path.addQuadCurve(to: CGPoint(x: self.frame.width, y: self.frame.height), controlPoint: CGPoint(x: 0, y: self.frame.height))
}
override func draw(_ rect: CGRect) {
self.createLineWithCurve()
path.lineWidth = 3
// Specify a border (stroke) color.
UIColor.purple.setStroke()
path.stroke()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And the output I get looks quite good already:
However, as you can see, the quad-curve is fatter than the normal line. Also, the quad-curve seems to be cut off at the bottom. How can I fix that? I want the path to be completely smooth without looking fatter at some points or being cut-off somehow.
Thanks in advance for your help :)
The problem is that your path is getting clipped by the bounds of the view. For example, in your vertical stroke starting at 0, 0, half of the path’s vertical stroke will fall outside of the left edge of the view.
You should use insetBy(dx:dy:) to inset the stroked path by half the line width, which will ensure the whole stroke will fall within the bounds of the view. And then use minX, maxX, etc. of that resulting inset CGRect to figure out the various CGPoint for your path.
For example:
class IndicatingView: UIView {
func lineWithCurve() -> UIBezierPath {
let lineWidth: CGFloat = 3
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath()
path.lineWidth = lineWidth
path.lineCapStyle = .square
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - rect.width))
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY), controlPoint: CGPoint(x: rect.minX, y: rect.maxY))
return path
}
override func draw(_ rect: CGRect) {
UIColor.purple.setStroke()
lineWithCurve().stroke()
}
}
That yields:
here you see my "result" -> i made the linewidth a bit bigger that you can see, it paints perfect ;)
Apple draws always exactly on point, so if you paint a line of width 3 on point 0,0, one point is -1,0, one on 0,0 and one on 1,0
Related
This example I am drawing a line and I want to add an arrow on the tip of it. The arrow function works fine when I use it in draw function but it is not being added to the view when I call it later on. Is there some way for me to add it later on ?
**My BezierView Class
**
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
currentPoint = line1.currentPoint
}
func drawArrow() {
let point = currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
**My View Controller
**
override func viewDidLoad() {
super.viewDidLoad()
let bezierView = BezierView(frame: CGRect(x: 0, y: 40, width: self.view.frame.width, height: 200))
bezierView.drawArrow()
}
I have tried to use: self.setNeedsDisplay(), self.setNeedsLayout(), self.layoutSubviews() functions in the drawArrow function but they didn't change the outcomes.
The reason why filling and stroking a bezier path draws things on the screen is because the draw(_:) method is called (by iOS) in a graphics context created by the system that allows you to draw on the screen. After draw(_:) returns, the graphics context ends, and you can no longer draw things on the screen.
Therefore, calling drawArrow in viewDidLoad does not work. You are not in that specific graphics context. Unfortunately, you cannot get this graphics context from outside of draw(_:) and draw in it.
In other words, all the drawing has to be done in draw(_:). If you just want to draw one arrow, you can just keep a boolean to store whether the arrow has been drawn or not:
final class BezierView: UIView {
var isArrowDrawn = false {
didSet {
// this causes draw(_:) to be called again
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
if isArrowDrawn {
let point = line1.currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
isArrowDrawn = true
}
}
If you want multiple arrows, to be drawn whenever drawArrow is called, then you would need to keep track of the list of points at which the arrows all are located. Something like
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
var arrowPoints = [CGPoint]() {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// ... omitted for brevity
currentPoint = line1.currentPoint
for point in arrowPoints {
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
arrowPoints.append(currentPoint)
}
}
Basically, you need to keep track of all the "states" that your view can be in, and your draw(_:) method is responsible for drawing the current state.
If you find this quite annoying, consider switching to using layers instead. You can, at any time, create a CAShapeLayer with a path, and add it as a sublayer as self.layer. Here is a tutorial to get started with.
I have an extension to curve the bottom edge of views since this styling is used on multiple screens in the app I am trying to create.
However, I have noticed I can only make it work with views that I have added trough interface builder. If I try to apply it on view created programmatically they do not render.
I have created a simple example to illustrate the problem. The main storyboard contains two viewControllers with a single colored view in the middle: one created with Interface Builder while the other programmatically.
In StoryboardVC, the view with the curve is rendered correctly without any problem. The setBottomCurve() method is used to create the curve.
If you compare this to setting the entry point to ProgrammaticVC, running the app you can see a plain white screen. Comment this line out to see the view appear again.
This is the extension used:
extension UIView {
func setBottomCurve(curve: CGFloat = 40.0){
self.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - curve
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I expect ProgrammaticVC to look identical to StoryboardVC (except for the difference in color)
The example project can be found here:
https://github.com/belamatedotdotipa/CurveTest2
I suggest to create a subclass instead of using an extension, this is a specific behaviour.
In this case you cannot see the result expected, when you are adding the view programmatically, because in the viewDidLoad you don't have the frame of your view, in this example you can use the draw function:
class BottomCurveView: UIView {
#IBInspectable var curve: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
override func draw(_ rect: CGRect) {
setBottomCurve()
}
private func setBottomCurve() {
let rect = bounds
let y: CGFloat = rect.size.height - curve
let curveTo: CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I am able to create triangle view in swift using below code-
class TriangleView: UIView {
override func draw(_ rect: CGRect) {
// Get Height and Width
let layerHeight = layer.frame.height
let layerWidth = layer.frame.width
// Create Path
let bezierPath = UIBezierPath()
// Draw Points
bezierPath.move(to: CGPoint(x: 0, y: layerHeight))
bezierPath.addLine(to: CGPoint(x: layerWidth, y: layerHeight))
bezierPath.addLine(to: CGPoint(x: layerWidth / 2, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: layerHeight))
bezierPath.close()
// Apply Color
UIColor.red.setFill()
bezierPath.fill()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
layer.mask = shapeLayer
}
}
override func viewDidLoad() {
super.viewDidLoad()
let triangle = TriangleView(frame: CGRect(x: 0, y: 0, width: 55 , height: 60))
triangle.backgroundColor = .white
triangle.transform = CGAffineTransform(rotationAngle: .pi * 4.49)
imageView.addSubview(triangle)
}
Now problem is that I want to change it to the right angle triangle using transform properties. How to achieve this?
Expected Result
Current Result
I hope you want this part.
Here is the code.
override func draw(_ rect: CGRect) {
let path = UIBezierPath.init()
// top left
path.move(to: .zero)
// top right
path.addLine(to: CGPoint(x: bounds.size.width,
y: 0))
// bottom left offset
path.addLine(to: CGPoint(x: bounds.size.width * 0.1,
y: bounds.size.height))
// bottom left
path.addLine(to: CGPoint(x: 0,
y: bounds.size.height))
// top left
path.close()
// change fill color here.
UIColor.black.setFill()
path.fill()
}
Result
How I drew
Modification
If you change the code UIColor.black.setFill() to
let fillColor = UIColor(red: 0xF9/0xff, green: 0x0D/0xff, blue: 0x5A/0xff, alpha: 1)
fillColor.setFill()
you get this result.
Have fun coding.
You're currently drawing 'a'. You want to draw 'b'.
extension UIBezierPath {
convenience init(rightAngledTriangleInRect: CGRect, offset withOffset: CGFloat) {
self.init()
move(to: CGPoint(x: rect.minX, y: rect.minY))
addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
addLine(to: CGPoint(x: rect.minX + offset, y: rect.maxY))
addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
addLine(to: CGPoint(x: rect.minX, y: rect.minY))
close()
}
}
Then use by:
let path = UIBezierPath.init(rightAngledTriangleInRect: rect, withOffset: 20.0)
Note that there's no need to add self. in the convenience inits for the bezier path instructions.
If you're doing a lot of drawing, I'd recommend adding a number of these inits to make life easier.
To use in your current TriangleView:
class TriangleView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath.init(rightAngledTriangleInRect: rect, withOffset: 20.0)
let shape = CAShapeLayer()
shape.path = path.cgPath
// Set the mask
layer.mask = shape
}
}
There are some easy way too for achivieng this but for using transform here is the sample code.use this transform instead of yours.it's sample code for having right angle triangle from your drawing:
triangle.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi).translatedBy(x: (triangle.bounds.width / 2), y: 0.0)
you should notice that CGAffineTransform rotate the view from it's origins
I want to create a UIView that has rounded borders which I have specified. For example, I want to create a UIView that has 3 borders: left, top and right, and the topright and topleft borders should be rounded.
This let's me create resizable border views (without rounded corners):
open class ResizableViewBorder: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
open override func draw(_ rect: CGRect) {
let edges = ... get some UIRectEdges here
let lineWidth = 1
if edges.contains(.top) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: 0 + lineWidth / 2),
CGPoint(x: self.bounds.width, y: 0 + lineWidth / 2)
])
}
if edges.contains(.bottom) || edges.contains(.all) {
addBezierPath(paths: [
CGPoint(x: 0, y: self.bounds.height - lineWidth / 2),
CGPoint(x: self.bounds.width, y: self.bounds.height - lineWidth / 2)
])
}
if (edges.contains(.left) || edges.contains(.all) || edges.contains(.right)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: 0 + lineWidth / 2, y: 0),
CGPoint(x: 0 + lineWidth / 2, y: self.bounds.height)
])
}
if (edges.contains(.right) || edges.contains(.all) || edges.contains(.left)) && CurrentDevice.isRightToLeftLanguage{
addBezierPath(paths: [
CGPoint(x: self.bounds.width - lineWidth / 2, y: 0),
CGPoint(x: self.bounds.width - lineWidth / 2, y: self.bounds.height)
])
}
}
private func addBezierPath(paths: [CGPoint]) {
let lineWidth = 1
let borderColor = UIColor.black
let path = UIBezierPath()
path.lineWidth = lineWidth
borderColor.setStroke()
UIColor.blue.setFill()
var didAddedFirstLine = false
for singlePath in paths {
if !didAddedFirstLine {
didAddedFirstLine = true
path.move(to: singlePath)
} else {
path.addLine(to: singlePath)
}
}
path.stroke()
}
}
However, I can not find a nice robust way to add a corner radius to a specified corner. I have a hacky way to do it with a curve:
let path = UIBezierPath()
path.lineWidth = 2
path.move(to: CGPoint(x: fakeCornerRadius, y: 0))
path.addLine(to: CGPoint(x: frame.width - fakeCornerRadius, y: 0))
path.addQuadCurve(to: CGPoint(x: frame.width, y: fakeCornerRadius), controlPoint: CGPoint(x: frame.width, y: 0))
path.addLine(to: CGPoint(x: frame.width, y: frame.height - fakeCornerRadius))
path.stroke()
Which gives me this:
Why is that line of the quad curve so fat? I prefer using UIBezierPaths over CALayers because CALayers have a huge performance impact...
What's wrong with your curve-drawing code is not that the curve is fat but that the straight lines are thin. They are thin because they are smack dab on the edge of the view. So your line width is 2 points, but one of those points is outside the view. And points are not pixels, so what pixels are there left to fill in? Only the ones inside the view. So the straight lines have an apparent visible line width of 1, and only the curve has a visible line width 2.
Another problem is that you probably are looking at this app running in the simulator on your computer. But there is a mismatch between the pixels of the simulator and the pixels of your computer monitor. That causes numerous drawing artifacts. The way to examine the drawing accurately down to the pixel level is to use the simulator application's screen shot image facility and look at the resulting image file, full-size, in Preview or similar. Or run on a device and take the screen shot image there.
To demonstrate this, I modified your code to operate in an inset version of the original rect (which, by the way, should be your view's bounds, not its frame):
let fakeCornerRadius : CGFloat = 20
let rect2 = self.bounds.insetBy(dx: 2, dy: 2)
let path = UIBezierPath()
path.lineWidth = 2
path.move(
to: CGPoint(x: rect2.minX + fakeCornerRadius, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX - fakeCornerRadius, y: rect2.minY))
path.addQuadCurve(
to: CGPoint(x: rect2.maxX, y: rect2.minY + fakeCornerRadius),
controlPoint: CGPoint(x: rect2.maxX, y: rect2.minY))
path.addLine(
to: CGPoint(x: rect2.maxX, y: rect2.maxY - fakeCornerRadius))
path.stroke()
Taking a screen shot from within the Simulator application, I got this:
As you can see, this lacks the artifacts of your screen shot.
I attached the View image. I want to achieve the small cut in bottom between the buy button and above flight information view.
I think the easiest way would be to create 2 circles as plain UIView instances and set their center as the left and right edges of the parent view respectively.
Since you set clipsToBounds to true, they will be clipped and only half of them will be visible on the screen.
public class TestView: UIView {
private let leftCircle = UIView(frame: .zero)
private let rightCircle = UIView(frame: .zero)
public var circleY: CGFloat = 0
public var circleRadius: CGFloat = 0
public override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(leftCircle)
addSubview(rightCircle)
}
public override func layoutSubviews() {
super.layoutSubviews()
leftCircle.frame = CGRect(x: -circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
leftCircle.layer.masksToBounds = true
leftCircle.layer.cornerRadius = circleRadius
rightCircle.frame = CGRect(x: bounds.width - circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
rightCircle.layer.masksToBounds = true
rightCircle.layer.cornerRadius = circleRadius
}
}
I've created a sample project demonstrating that. Here is how it looks in my simulator (iPhone SE 11.2):
I had to do this with shadows. I tried creating a layer and subtracting another layer from it using evenOdd fillRule, however that didn't work since I need a specific path for shadows and evenOdd applies to the filling in the path instead.
In the end I just created the path manually
func setShadowPath() {
let path = UIBezierPath()
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: cutoutView.frame.minX, y: bounds.minY))
path.addArc(withCenter: CGPoint(x: cutoutView.frame.midX, y: bounds.minY),
radius: cutoutView.bounds.width/2, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
layer.shadowPath = path.cgPath
}
I created a "cutoutView" in my xib so I could trace around it easily.
That makes the shadow the correct shape, and then in order to create the cut itself I just created a layer using that same path
func setupBackground() {
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = layer.shadowPath
backgroundLayer.fillColor = UIColor.white.cgColor
layer.insertSublayer(backgroundLayer, at: 0)
}