draw#rect core graphics, on top of a layer? - ios

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.

Related

Weird behaviour touch ended called automatically

I am rendering the current screen (map screen) into a smaller view with magnifying effect.
Problem illustration
My app taking 110 MB memory. As soon as I start dragging the annotation, memory reaches to 130-140 MB then touchesEnded is called forcefully by system.
Expected behaviour
App should not call touchesEnded automatically.
What I did in dragging of annotation is added a magnified view into smaller view. Below is my code of draw method.
public override func draw(_ rect: CGRect) {
UIGraphicsBeginImageContextWithOptions(self.magnifiedView!.bounds.size, false, 0.0);
guard let context = UIGraphicsGetCurrentContext() else { return }
context.translateBy(x: radius, y: radius)
context.scaleBy(x: scale, y: scale)
context.translateBy(x: -magnifiedPoint.x, y: -magnifiedPoint.y)
removeFromSuperview()
self.subviews.first?.removeFromSuperview()
magnifiedView?.layer.render(in: context)
let mapImage = UIGraphicsGetImageFromCurrentImageContext();
self.addSubview(UIImageView(image: mapImage))
magnifiedView?.addSubview(self)
UIGraphicsEndImageContext();
}
As soon as the user starts dragging the annotation I started calling setNeedDisplay which triggers the draw method again and again. I don't know what to do now. I am totally stuck at this point.
The biggest issue you have is that you shouldn't be overriding draw at all. All this code does is update an image view's image property. draw is not the correct place for this. draw should only be used to actually draw content into self. It shouldn't be used to update subviews. It shouldn't add or remove views. It definitely shouldn't be used to remove self from the view hierarchy.
Certainly the call to removeFromSuperview() is not helping. That is likely why the drag is being cancelled. You need to leave the view in place.
Leave the image view in place. Leave this view in place. Generate the new image and update the image view's image property with the new image. Don't remove any views. Don't add any views during that process.
Put the code to generate and update the new image into a function that you can call as the drag operation moves around. This could be in the handler for a pan gesture recognizer if that's what you are using. Or it could be in touchesMoved(with:) if you are overriding the various touches... methods.

draw rounded lines iOS Swift

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

When draw rect is overridden in iOS, why are translated UIBezierPaths treated differently for sizing a custom UIView?

I override draw rect in a custom UIView class:
override func draw(_ rect: CGRect) { ...
Then, I draw into it like this:
func drawWithGradient(path:UIBezierPath,linearGradient:LinearGradientAttributeSet) {
let context = self
context.saveGState()
path.addClip()
context.drawLinearGradient(
linearGradient.gradient,
start:linearGradient.start,
end:linearGradient.end,
options:linearGradient.options
)
context.restoreGState()
}
And that works great, the UIView fits the size of the UIBezierPath perfectly.
But, I wanted a bit more space above the path, to draw some more things. So, I moved the path down using an affine transform. (This is necessary because there are a lot of images and the amount of space will vary.)
let shiftdownAffine = CGAffineTransform(translationX: 0, y: roomAbove)
thePath.apply(shiftdownAffine)
I didn't change anything else, but the magic was gone. Although I passed the new (translated) path into drawWithGradient, the customUIView matches the size of the original UIBezierPath, while I expected it to behave exactly as though I changed the original UIBezierPath directly through drawing code. With the translated UIBezierPath, the path is drawn at the new location, but the size of the UIView is the same, the so the path gets cut off.
Can you explain why this is happening and/or how to make a translated path that would be treated the same as a drawn path?

Remove line drawn on view

I am new to CoreGraphics . I am trying to create view which contains two UIImageview added in scrollview programatically. After adding it i want to connect both center with line. I have used bezier path as well as CAShapelayer. But line is drawn on UIImageview also so i want to remove line above UIImageview or send line to back to UIImageview. I have done below code.
let path: UIBezierPath = UIBezierPath()
path.moveToPoint(CGPointMake(personalProfile.center.x, personalProfile.center.y))
path.addLineToPoint(CGPointMake(vwTwo.center.x, vwTwo.center.y))
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.blueColor().CGColor
shapeLayer.lineWidth = 3.0
shapeLayer.fillColor = UIColor.clearColor().CGColor
self.scrollView.layer.addSublayer(shapeLayer)
Please also check screenshot, i want to remove red marked portion of blue line .
You can do this simply by reducing the zPosition of your shapeLayer
This will allow the layer to be drawn underneath your two views (and far easier than trying to calculate a new start and end point of your line). If you look at the documentation for zPosition:
The default value of this property is 0. Changing the value of this property changes the the front-to-back ordering of layers onscreen. Higher values place this layer visually closer to the viewer than layers with lower values. This can affect the visibility of layers whose frame rectangles overlap.
Therefore, as it defaults to 0, and UIViews are just wrappers for CALayers, you can use a value of -1 on your shapeLayer in order to have it drawn behind your other views.
For example:
shapeLayer.zPosition = -1
Side Note
Most of the time in Swift, you don't need to explicitly supply a type when defining a variable. You can just let Swift infer it. For example:
let path = UIBezierPath()
I would see 2 options, an easy and a harder option.
Move the UIImageView to the front after drawing the line, effectively hiding the line behind the UIImageView.
Calculate the points at which you want the line to start and end and draw a line from these points instead of the centers.

CGPaths are always being drawn behind sublayer

I was drawing a background image in the Draw method with several paths (CGPath) over it.
To avoid drawing the background image every time a setNeedDisplay is called (when a path changes), I've created a layer with the content = background image.
This layer is added when the background image is set:
Layer.InsertSublayer(backgroundLayer, 0);
And the paths are drawn in the Draw method:
public override void Draw(RectangleF rect)
{
base.Draw(rect);
using (CGContext context = UIGraphics.GetCurrentContext())
{
CGPath path = new CGPath();
...
context.DrawPath(CGPathDrawingMode.FillStroke);
}
}
It seems that the drawing of the paths is being made in the "main" Layer, that is the superlayer of the background layer that I create (this is my assumption, don't know for sure).
Is it possible to draw the shapes in a different Layer (this way I could add a second layer to the superlayer and change the sublayer zposition)?
Thanks for your help,
L. Pinho

Resources