How to draw a sloping oval with UIBezierPath - ios

I wanna draw a sloping oval with UIBezierPath. There is only a method UIBezierPath.init(ovalInRect: rect) to draw a oval without slop.What can I do to draw this? thanks

Here is an example of a rotated oval:
class MyView: UIView {
override func draw(_ rect: CGRect) {
// create oval centered at (0, 0)
let path = UIBezierPath(ovalIn: CGRect(x: -75, y: -50, width: 150, height: 100))
// rotate it 30 degrees
path.apply(CGAffineTransform(rotationAngle: 30 * .pi / 180))
// translate it to where you want it
path.apply(CGAffineTransform(translationX: self.bounds.width / 2, y: self.bounds.height / 2))
// set stroke color
UIColor.blue.setStroke()
// stroke the path
path.stroke()
}
}
let view = MyView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .white
Here it is running in a Playground:

Related

UIBezierPath does not change color and is not added to the view

There is an implementation like this:
class EllupseTest: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let views = MyView(frame: CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93))
views.backgroundColor = .clear
views.rotate(degrees: 45.84)
view.addSubview(views)
}
}
class Ellipse: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 303.19, height: 495.93))
UIColor.cyan.setFill()
path.fill()
}
}
But if I want to change the color, the rectangle will be colored, not the ellipse itself (that's why I put views.backgroundColor = .clear for a while) How can I change the color of the ellipse itself, and not its rectangle?
I read that you can use PaintCode, but I don't understand how to add it to the view via view.addSubview (). How to add to a view
Here is the code from PaintCode
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
//// Color Declarations
let color = UIColor(red: 1.000, green: 0.773, blue: 0.620, alpha: 1.000)
//// Oval Drawing
context.saveGState()
context.translateBy(x: 610, y: -139)
context.rotate(by: 90 * CGFloat.pi/180)
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 314.87, height: 515.04))
color.setFill()
ovalPath.fill()
context.restoreGState()
I work with UIBezierPath for the first time)

How to add class UIView in addSubview

I have a code that draws an ellipse, but it's in a separate class that inherits from UIView
class DRAW: UIView {
override func draw(_ rect: CGRect) {
var path = UIBezierPath()
path = UIBezierPath(ovalIn: CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93))
UIColor.green.setFill()
path.stroke()
path.fill()
}
}
If I try to add it via addSubview(), then it is not on the screen, and if I give it the dimensions, then a black crawl for the entire display and only in the middle is the ellipse I need.
view.addSubview(DRAW.init(CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93)))
How do I display only an ellipse without a black square. I would be grateful for your help
You can just simply set the clear background color of your view
let yourView = DRAW.init(frame: CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93))
yourView.backgroundColor = .clear
self.view.addSubview(yourView)

Why the edges of UIView are not smooth for huge corner radius?

class AttributedView: UIView {
private let gradient = CAGradientLayer()
var cornerRadius: CGFloat = 3 {
didSet {
layer.cornerRadius = cornerRadius
}
}
}
I simply use it for both: button view (corner radius: 20) and background circle (corner radius: 600).
Why button is smooth, and background is not?
With iOS 13.0 you can simple do, in addition to setting corner radius
yourView.layer.cornerCurve = .continuous
You should use bezzier paths and draw circle. After that you will receive nice, smooth edges.
let context = UIGraphicsGetCurrentContext()!
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.white.cgColor] as CFArray, locations: [0, 1])!
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 64, y: 9, width: 111, height: 93))
context.saveGState()
ovalPath.addClip()
context.drawLinearGradient(gradient, start: CGPoint(x: 119.5, y: 9), end: CGPoint(x: 119.5, y: 102), options: [])
context.restoreGState()
UIBezierPath is a simple and efficient class for drawing shapes using Swift, which you can then put into CAShapeLayer, SKShapeNode, or other places. It comes with various shapes built in, so you can write code like this to create a rounded rectangle or a circle:
let rect = CGRect(x: 0, y: 0, width: 256, height: 256)
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 50)
let circle = UIBezierPath(ovalIn: rect)
You can also create custom shapes by moving a pen to a starting position then adding lines:
let freeform = UIBezierPath()
freeform.move(to: .zero)
freeform.addLine(to: CGPoint(x: 50, y: 50))
freeform.addLine(to: CGPoint(x: 50, y: 150))
freeform.addLine(to: CGPoint(x: 150, y: 50))
freeform.addLine(to: .zero)
If your end result needs a CGPath, you can get one by accessing the cgPath property of your UIBezierPath.
You probably should clip bounds of this view:
attributedView.clipsToBounds = true

How do I draw stuff under the text of a UILabel?

I created a custom UILabel subclass that has a circle in the middle, and the label's text (which is a number) will be on top of the circle.
I initially thought of doing this using layer.cornerRadius, but that will not create a circle when the label's width and height are not equal.
What I mean is, for a label with width 100 and height 50, I still want a circle with radius 50 and centre at (50, 25).
Therefore, I tried to use UIBezierPath to draw the circle. This is what I have tried:
override func draw(_ rect: CGRect) {
super.draw(rect)
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
}
I have put super.draw(rect) because I thought that would draw the label's text, but when I run the app, I only see the circle and not my label text.
I am very confused because why hasn't super.draw(rect) drawn the label's text?
The text is not seen because the "z-index" of UIBezierPaths depends on the order in which they are drawn. In other words, UIBezierPaths are drawn on top of each other.
super.draw(rect) indeed draws the text. But when you put it as the first statement, it will get drawn first, so everything you draw after that, goes on top of the text. To fix this, you should call super.draw(rect) last:
override func draw(_ rect: CGRect) {
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
super.draw(rect) // <------- here!
}
Alternatively, just subclass UIView, draw the circle in draw(_:), and add a UILabel as a subview of that. The advantage if this approach is that it does not depend on the implementation of super.draw(_:), which might change in the future,

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.

Resources