Im trying to make an app which draws a fractal tree. I managed to make the code that generates all the start and end points from all the lines. I also managed to draw the lines but right now they are really boxy and want them to have rounded corners.
I using a UIView and using UIBezierPaths to draw the lines inside the view draw function. To retrieve the points I have an array of Branch objects inside a sigleton class. A branch object has among other things a startingPoint and a ending point which are both tuples( (x: Double, y: Double) ).
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setStroke()
for branch in Tree.shared.branches{
let path = UIBezierPath()
print(branch.startingPoint)
print(branch.endingPoint)
path.move(to: CGPoint(x: branch.startingPoint.x, y: branch.startingPoint.y))
path.addLine(to: CGPoint(x: branch.endingPoint.x, y: branch.endingPoint.y))
path.lineWidth = 3
path.stroke()
}
}
How could i make the corners rounded?
Also if someone knows a free library that could facilitate this Im also interested.
edit: Im not interested in how to generate the tree, I have done already done this part of the code I need help with drawing the lines.
You don't need a library, you just need to spend a little time learning how to draw curves with UIBezierPath, and curves are one of the things that that class is best at. A key to drawing curves is understanding how control points work. Here's an answer I wrote some time ago about how to smoothly connect curved lines, which I think will help. Play around with -addCurveToPoint:controlPoint1:controlPoint2:.
If you don't actually want curves, but really just want the corners to be rounded rather than pointy, then all you need to do is to set the lineJoinStyle to kCGLineJoinRound.
About rounded corners of some view, is just set the parameters below:
func adjustView(_ view: UIView){
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.red
view.layer.cornerRadius = 10
view.clipsToBounds = true
view.backgroundColor = UIColor.white
}
If you wish more information check the current documentation about layer property:
https://developer.apple.com/documentation/uikit/uiview/1622436-layer
Related
Say you have this in a UIView,
override func draw(_ rect: CGRect) {
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
... lots of complicated drawing here, sundry paths, colors, widths etc ...
c?.strokePath()
}
Of course, it will draw the hell out of your drawing for you.
But say in the same UIView you do this ...
func setup() { // in inits, layout
if nil etc {
thingLayer = CAShapeLayer()
self.layer.insertSublayer(thingLayer, at: 0)
thingLayer.fillColor = UIColor.yellow.cgColor }
thingLayer.frame = bounds
let path = ... some fancy path
thingLayer.path = path.cgPath
}
Indeed, the new yellow layer is drawn over the drawing in draw#rect.
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Core graphics commands:
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(10.0)
c?.move(to: CGPoint(x: 10.0, y: 10.0))
c?.addLine(to: CGPoint(x: 40.0, y: 40.0))
c?.strokePath()
seem to draw to a place directly above or on the main .layer
(Well that appears to be the case, as far as I can see.)
How do you draw - using core graphics commands - either on to thingLayer, or perhaps on to another layer on top of all??
Surely in draw#rect you can specify which cgLayer to draw to?
In draw#rect, can you make another cgLayer and draw to context, that cgLayer?
Bottom line, the draw(_:) is for rendering the root view of a UIView subclass. If you add sublayers, they will be rendered on top of whatever is done in the root view’s draw(_:).
If you have a few paths currently performed in the draw(_:) that you want to render on top of the sublayer that you’ve added, you have a few options:
Move the paths to be stroked in their own CAShapeLayer instances and add them above the other sublayer that you’ve already added.
Consider the main UIView subclass as a container/content view, and add your own private UIView subviews (potentially with the “complicated” draw(_:) methods). You don’t have to expose these private subviews if you don’t want to.
Move the complicated drawing to the draw(in:) of a CALayer subclass and, again, add this sublayer above the existing sublayer that you’ve already created.
Given that the thingLayer is, effectively, only setting the background color, you could just set the background color of the view and not use a layer for this background color at all.
The CALayer hierarchy is a tree, where a root layer is first (e.g. the layer that comprises the background of a window), followed by its sublayers (an array of CALayer objects, stored and drawn in in back-to-front order), followed by their sublayers, and so on, recursively.
Your implementation of UIView.draw(_ rect: CGRect) is defining how the main layer of your view (self.layer) is drawn.
By adding your CAShapeLayer as a sublayer of self.layer, you're making it be draw after self.layer, which has the effect of it being drawn above self.layer, which contains the line you drew in UIView.draw(_ rect: CGRect).
To resolve this, you need to put your stroke in sublayer after your thingLayer.
self.layer.insertSublayer(thingLayer, at: 0)
self.layer.insertSublayer(lineLayer, above: thingLayer)
By the way, you forgot to delegate to super.draw(rect). It's not necessary for direct subclasses of UIView (since the base implementation doesn't do anything), but it is necessary for every other case, and it's generally a good habit to get into (lest you run into really obscure/frustrating drawing bugs).
If your yellow shape layer doesn't change or move around independently, you could get rid of the CAShapeLayer and just draw the shape yourself at the top of your implementation of draw(_:):
let path = // your fancy UIBezierPath
UIColor.yellow.setFill()
path.fill()
Otherwise, as other commenters have said, you'll need to move your drawing code into additional custom subclasses of UIView or CALayer, and stack them in the order you want.
I'm struggling to understand how to achieve the following component:
Theory:
I've managed to create 4 draggable UIView'. When one of the UIView change position I'm creating UIBezierPath from connecting each UIView center, to a box shape and displaying it using CAShapeLayer.
I can't understand how to calculate the "addQuadCurve" control point to achieve the curved lines in the illustration.
Current Code:
func updateLines() {
let path = UIBezierPath()
path.move(to: v.center)
path.addLine(to: v2.center)
path.addLine(to: v4.center)
path.addLine(to: v3.center)
path.close()
line.path = path.cgPath
line.strokeColor = UIColor.black.cgColor
line.fillColor = UIColor.red.cgColor
}
Any help or advice for the right direction will be highly appreciated.
Best regards,
Roi
It looks to me like a connected set of cubic Bezier curves where the beginning and end are the same point. If you watch the animation, it looks like as you drag different points, the point that's the beginning/end of the curve gets changed.
Watch carefully and you'll see that 3 of the 4 corners are smooth curves, and one has a "kink" in it. The kinked corner seems to be a point other than the one that's being moved. That's probably the begin/end point.
I am currently trying to develop a drawing app. The problem that I've come across is optimising the app's performance.
I have subclassed an UIView, in which I am detecting user's inputs.
At first I tried to draw all the lines with CoreGraphics in draw(_ rect: CGRect) method but when the number of lines was 10 000+, the lag was very noticeable. So then I thought of creating a CGImage that I would make after every draw(...) cycle and then when the next draw cycle came, I would put just the new lines on top of that CGImage, therefore not drawing ALL previous lines again and saving precious time.
I am registering new lines in touchesBegan() and touchesMoved() methods and then I am calling self.needsDisplay().
This is the code that I am currently using:
var mainImage: CGImage?
var newLines: [Line]
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
//context?.translateBy(x: 0, y: bounds.height)
//context?.scaleBy(x: 1, y: -1)
if let image = mainImage {
context?.draw(image, in: bounds)
}
for line in newLines {
context?.setStrokeColor(UIColor.black.cgColor)
context?.setLineWidth(1)
context?.move(to: line.previousLocation)
context?.addLine(to: line.location)
context?.strokePath()
}
newLines = []
mainImage = context?.makeImage()
}
Now the problem with it is that the lines I draw are divided in two parts and are mirrored vertically. (Image split vertically). I have drawn here two continuous lines - one straight line and one curved line. The weird thing is that if I flipped half of the UIView, the lines would be aligned continuously with no space between them.
If I uncommented the two lines from my previously mentioned code (context?.translateBy(x: 0, y: bounds.height) and context?.scaleBy(x: 1, y: -1)), the image would look like this.
The only wrong thing now is that I was drawing on the lower side of the screen and getting results on the upper one.
How do I correct it? What is the proper solution for my problem?
Thank you very much!
When you comment out those two lines, then you are NOT making any changes to the graphics context. If you un-comment them, then you ARE changing the context at each call and not saving it. So I think you're essentially flipping it each time. Not sure if it's that but you should definitely be pushing/saving your context before drawing (and obviously restoring/popping it).
Check out this post: using UIImage drawInRect still renders it upside down.
Is there a general method to fill an area in Swift.
I have various shape in my UIView, lines, ellipses, curves … etc.
Those shapes delimit a certain number of areas.
I want to fill one area in a color (uiColorOne) and another area in another color (uiColorTwo).
Is that possible? If YES, how can I do that?
import UIKit
override func drawRect(rect: CGRect)
{
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 200, y: 200))
path.addLineToPoint(CGPoint(x: 200, y: 300))
path.addLineToPoint(CGPoint(x: 300, y: 200))
path.closePath()
UIColor.redColor().set()
path.lineWidth = 1
path.fill()
}
You will need to override drawRect in order to start making some geometric shaped items in your code (ONLY OVERRIDE AND NEVER CALL IT). The code snippet creates an element based on points (careful not pixels) and fills it with the redColor.You will have to use UIBezierPath most of the times you are playing with elements and so I suggest you have a really good look at it from apple documentation here, since it has some awesome methods that will help you out with your every day struggle : UIBezierPath
I'm sure this is a very simple thing to do, but I can't seem to wrap my head around the logic.
I have two UIViews. One black, semi-transparent and "full-screen" ("overlayView"), another one on top, smaller and resizeable ("cropView"). It's pretty much a crop-view setup, where I want to "dim" out the areas of an underlying image that are not being cropped.
My question is: How do I go about this? I'm sure my approach should be with CALayers and masks, but no matter what I try, I can't get behind the logic.
This is what I have right now:
This is what I would want it to look like:
How do I achieve this result in Swift?
Although you won't find a method such as subtract(...), you can easily build a screen with an overlay and a transparent cut with the following code:
Swift 4.2
private func addOverlayView() {
let overlayView = UIView(frame: self.bounds)
let targetMaskLayer = CAShapeLayer()
let squareSide = frame.width / 1.6
let squareSize = CGSize(width: squareSide, height: squareSide)
let squareOrigin = CGPoint(x: CGFloat(center.x) - (squareSide / 2),
y: CGFloat(center.y) - (squareSide / 2))
let square = UIBezierPath(roundedRect: CGRect(origin: squareOrigin, size: squareSize), cornerRadius: 16)
let path = UIBezierPath(rect: self.bounds)
path.append(square)
targetMaskLayer.path = path.cgPath
// Exclude intersected paths
targetMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
overlayView.layer.mask = targetMaskLayer
overlayView.clipsToBounds = true
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.black
addSubview(overlayView)
}
Just call this method inside your custom view's constructor or inside your ViewController's viewDidLoad().
Walkthrough
First I create a raw overlayView, then a CAShapeLayer which I called "targetMaskLayer". The ultimate goal is to draw a square with the help of UIBezierPath inside that overlayView. After defining the square's dimensions, I set its cgPath as the targetMaskLayer's path.
Now comes an important part:
targetMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
Here I basically configure the fill rule to exclude the intersection.
Finally, I provide some styling to the overlayView and add it as a subview.
ps.: don't forget to import UIKit
There might be another drawing solution but basically you have 4 areas that need to be handled. Take the square area above and below the space with full width and add the right and left side between them with constraints to eachother.