Filing UIBezierPath in Swift - ios

How can I color the inside of the UIBezier path? I am making a simple triangle and want to color inside of the triangle with red color.
class GraphView : UIView {
override func drawRect(rect: CGRect) {
UIColor.redColor().setFill()
UIColor.greenColor().setStroke()
let path = UIBezierPath(rect: rect)
path.moveToPoint(CGPointMake(100,620))
path.addLineToPoint(CGPointMake(300,100))
path.addLineToPoint(CGPointMake(500,620))
path.addLineToPoint(CGPointMake(100,620))
path.closePath()
path.fill()
path.stroke()
}
}
let graphView = GraphView(frame: CGRectMake(0,0,960,640))
XCPlaygroundPage.currentPage.liveView = graphView
The above code is written in the playground: Here is the result:

The problem is with how you're creating your UIBezierPath. You're creating it with a rectangle of the view's bounds, and then adding a triangle inside that rectangle. Therefore they're both getting filled – resulting in the entire view being red.
If you set usesEvenOddFillRule to true, you'll better see the result of the unwanted rectangle in your path:
If you just want to fill a triangle then replace this line:
let path = UIBezierPath(rect: rect)
with this:
let path = UIBezierPath()
This will create a new empty UIBezierPath that you can add your triangle to.

Related

How can I get the coordinates of a Label inside a view?

What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.

Using CAShapeLayer and removeFromSuperLayer

I am making an app, which should draw object from touch points.
I am using CAShapeLayerand UIBezierPath, but when I use removeFromSuperlayer() nothing happens.
I need to delete old shapes and draw just the new one.
I do not know why, but after one build and upload this app to my iPad, Xcode give me an error:
Cannot use optional chaining on non-optional value of type CAShapeLayer
Anyone who can help me?
And another question is:
How to draw just one line?
When I have 2 points, it draws nothing, the app draws just object after 3 points and more.
Here is a part of my code:
private func drawObj(){
let objectPath = UIBezierPath()
objectPath.move(to: CGPoint(x: pointsX[0], y: pointsY[0]))
let xx = pointsX.count - 1
print(xx)
for i in 1...xx {
objectPath.addLine(to: CGPoint(x: pointsX[i], y: pointsY[i]))
}
objectPath.close()
let object = CAShapeLayer()
object.removeFromSuperlayer()
object.path = objectPath.cgPath
object.fillColor = UIColor.red.cgColor
object.opacity = 0.2
self.view.layer.addSublayer(object)
}
If you create a new CAShapeLayer, there's no point in calling removeFromSuperlayer on that layer. You only do that for layers that have previously been added as sublayer of some other layer. So, don't call removeFromSuperlayer using this new layer that you just created, but save a reference to the previous one. That's the one that you should remove.
Or, as Dmitry pointed out, you should just add the shape layer once, and just update its path:
private var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.opacity = 0.2
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
return shapeLayer
}()
// add this to your view's layer in the logical place, e.g. `viewDidLoad`
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(shapeLayer)
}
// now your draw method only needs to update the path
private func drawObj() {
let objectPath = UIBezierPath()
objectPath.move(to: CGPoint(x: pointsX[0], y: pointsY[0]))
for i in 1 ..< pointsX.count {
objectPath.addLine(to: CGPoint(x: pointsX[i], y: pointsY[i]))
}
objectPath.close()
shapeLayer.path = object.cgPath
}
Note, I've also updated the lineWidth and the strokeColor of the shape layer so that if there are only two points and there is nothing to "fill", you can at least see the stroked path.
There is no need to remove the old CAShapeLayer from the parent layer and inserting a new one. Just create one CAShapeLayer, insert it to the parent layer. And then just update the 'path' parameter of it when you need to change it.

How to draw shapes like circles, oval, ractangles dynamically in iOS?

I have a UICollectionView of showing shapes like circles, oval, ractangles. So when I clicked on Cell I need to draw same shape on UIView and after I need to move and resize that view which was drawn by clicking in Cell. How it can achieve in iOS ? Thanks in advance.
If you use a UICollectionView, your reusable cell will have the "layer" to draw on.
What to do?
1. Create a UIView subclass and place it inside your reusable cell.
2. Override drawRect(_:) method, you'll do all the drawing inside.
3. Add your shape/line drawing code to drawRect method.
For example use UIBezierPath class for lines, arcs etc. You'll be able to create all sorts of shapes.
You should read the CoreGraphics API Ref:
https://developer.apple.com/reference/coregraphics
also learn about Layers, Contexts and drawRect:
A very basic example of a circle:
class CircleView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.addEllipse(in: rect)
context.setFillColor(.red.cgColor)
context.fillPath()
}
}
and drawing lines (rects, stars etc.) with UIBezier paths:
override func drawRect(rect: CGRect) {
var path = UIBezierPath()
path.moveToPoint(CGPoint(x:<point x>, y:<point y>))
path.addLineToPoint(CGPoint(x:<next point x>, y:<next point y>))
point.closePath()
UIColor.redColor().set()
point.stroke()
point.fill()
}
study about bezier path. It is possible with this. hope it will work for you.
You can use CAShapeLayer. eg. Draw a circle.
shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = aFrame;
UIBezierPath *spath = [UIBezierPath bezierPathWithArcCenter:centerPoint radius:12 startAngle:0 endAngle:(2 * M_PI) clockwise:YES];
shapeLayer.strokeColor = [UIColor colorWithRed:71/255.0 green:157/255.0 blue:252/255.0 alpha:1].CGColor;
shapeLayer.path = spath.CGPath;
shapeLayer.strokeStart = 0;
shapeLayer.strokeEnd = 0;
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
I hope it help you
You can use layer's properties:
cornerRadius
borderWidth
borderColor

BlurView below a button with fading edges

I created a button on top of my mapView.
In order to make that Button more visible I added a blur view below that button.
I don't like the sharp edges of my blur view.
How do I make the blur fade out slowly transitioning into the mapView?
EDIT: To be more specific. I mean a fading blurred gradient with round corners.
I think this can help you, first you need subclass your button and add this code in drawRect and replace UIColor.blueColor().CGColor by yourColor.CGColor
class UICustomBackgroundButton: UIButton {
override func draw(_ rect: CGRect) {
// Drawing code
super.draw(rect)
let ctx = UIGraphicsGetCurrentContext();
let path = CGPath(roundedRect: rect.insetBy(dx: self.frame.size.height/4, dy: self.frame.size.height/4) , cornerWidth: self.frame.size.height/8, cornerHeight: self.frame.size.height/8, transform: nil)
ctx?.setShadow(offset: CGSize.zero, blur: self.frame.size.height/4, color: UIColor.blue.cgColor);
ctx?.setFillColor(UIColor.blue.cgColor);
//if number of cicles is our glow is darker
for _ in 0..<6
{
ctx?.addPath(path);
ctx?.fillPath();
}
}
}
I Hope this helps you.
and this is how it look
Have you tried the cornerRadius property? That should remove the sharp edges

Failing to stroke() the UIBezierPath

I wish to create a perfectly rounded rect (Circle) and paint it on the screen. I have tested my code in playground, and it successfully paints the UIBezierPath. It doesn't, however, successfully paint it in the iOS simulator. Here's the code I've been working on:
class Circles {
//Defining the rounded rect (Circle)
func roundRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded the rectangle (Circle)
var roundedRect = UIBezierPath()
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Drawing the Bezier Path (Circle)
func drawRect(rect: UIBezierPath){
rect.moveToPoint(self.point)
UIColor.blackColor().setStroke()
rect.stroke()
}
//Giving the rounded rect (Circle) it's position
var point = CGPointMake(500, 500)
}
//Giving the rounded rect (Circle) it's size
var newRect = Circles().roundRect(200.0, angle: 7)
//Drawing the rounded rect (Circle)
Circles().drawRect(newRect)
I have seen some other posts with similar problems from a few years back, however they were in Objective-C, I tried translating but it was not of any use. I've also tried several other methods of painting the path on the screen but, again sadly, it was of no use. I tested it to make sure the functions are working with println statements, the issue is I don't know why the stroke is not activating. Thanks for reading, -Zach.
Here's the updated version using what Mike said:
class CircleView: UIView {
override func drawRect(rect: CGRect) {
// Creating the rectangle's size
var newRect = Circles().roundRect(200.0, angle: 7)
//Drawing the rectangle
Circles().drawRect(newRect)
}
//Holding all to do with the circle
class Circles {
//Defining the rounded rect (Circle)
func roundRect(radius: CGFloat, angle: CGFloat) -> UIBezierPath {
//Creating the rounded rect (Circle)
var roundedRect = UIBezierPath()
roundedRect.addArcWithCenter(CGPointZero, radius: radius,
startAngle: 0, endAngle: angle ,
clockwise: true)
return roundedRect
}
//Drawing the Bezier Path (Circle)
func drawRect(rect: UIBezierPath){
rect.moveToPoint(self.point)
UIColor.blackColor().setStroke()
UIColor.blackColor().setFill()
rect.stroke()
rect.fill()
}
//Giving the rounded rect (Circle) it's position
var point = CGPointMake(500, 500)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//Generating the background
self.view.backgroundColor = UIColor(patternImage: UIImage(named: "normalpaper.jpg"))
let circleView = CircleView(frame: self.view.bounds)
self.view.addSubview(circleView)
}
As you mentioned in the comments, you're calling drawRect from a UIViewController's viewDidLoad function. You don't have a valid drawing context there, so that's not going to work.
The easiest way to make this work is to create a UIView subclass with its own drawRect function which calls Circle().drawRect(...) and add that as a subview of your UIViewController's view.
Here's an example of this:
Note: I've made the custom view transparent by setting circleView.opaque = false so that the background you mentioned in the comments shows through.
class CircleView: UIView {
override func drawRect(rect: CGRect) {
// I just copied this directly from the original question
var newRect = Circles().roundRect(200.0, angle: 7)
Circles().drawRect(newRect)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Create a new CircleView that's the same size of this
// view controller's bounds and add it to the view hierarchy
let circleView = CircleView(frame: self.view.bounds)
circleView.opaque = false
self.view.addSubview(circleView)
}
}
Note: If you're going to be doing custom drawing, I highly recommend you read Drawing and Printing Guide for iOS. It'll teach you all the basics you need to make it work.

Resources