Drawing in iOS - Part 5: Custom Control | Ray Wenderlich Videos

Create an adjustable thermometer control using CAShapeLayers


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/4659-drawing-in-ios/lessons/5

Hi Caroline, once again I have a few questions which I hope you’ll find the time to answer:

  1. Just for clarification: Is the drawing not performed within the view’s draw(:) method because we are dealing with a CAShapeLayer, which basically knows how to draw itself?
  2. Also, could you explain why you are not designing the control from within layoutSubviews() as demonstrated before with the button and the tableview cell, especially because we are referring to the view’s width and height properties? And could you elaborate more on the use of that function in the context of layers? The documentation is not very helpful in that case.
  3. How come the layer is able to draw itself even though you didn’t give it any bounds to work with? Or are the bounds inferred from the UIView’s AutoLayout constraints?
  4. Shouldn’t the line lineWidth / 2 theoretically result in 1.5 points → 4.5 pixels on a high resolution screen and thus make the line blurry?

Thanks in advance!

  1. draw(_:) goes into the view’s layer.
class View: UIView {
  override func draw(_ rect: CGRect) {
    UIColor.green.setFill()
    let path = UIBezierPath(rect: rect)
    path.fill()
  }
}
let view = View(frame: CGRect(x: 0, y: 0, width: 500, height: 500))

This results in green at the end of the playground from part 4:

Screen Shot 2019-12-18 at 4.15.00 pm

If you take out the draw(_:), view.layer.contents will contain nil.

You generally use draw(_:) when you’re not using CALayers

  1. Theoretically, yes, you are correct. However, lineWidth is calculated from bounds / 3, so it’s already compromised. You can try different numbers, take a screenshot of the simulator and zoom in to see what you get, and then run it on your device to see whether the quality is acceptable.
  1. I can’t remember what my thought processes actually were. You may be correct, because we do want to resize when the view size changes. I may have thought that in this particular case, that the view size shouldn’t change - the view is square-ish? and the device won’t change during this session. So set it up once during awakeFromNib.

layoutSubviews, as you know, is called regularly. This is a good summary:

  1. thermoLayer’s bounds are always zero. You can print them to see. The path is being drawn outside of the layer’s bounds. You wouldn’t be able to perform a hit test on this layer.

It’s very kind of you that you took the time to respond to all my questions.
Regarding the above quote, doesn’t draw(_:)draw into the view’s main layer and thus omitting it would, as you explained, leave the main layer’s content to be nil? Maybe I got the concept wrong but I thought that draw(_:) or the delegate equivalent draw(_:in:)does work on CALayers while, in contrast, CAShapeLayers, CAGradientLayers etc. offer the “self-drawing” functionality, which is why we also don’t need a current context to commit our drawing to. I would be really grateful for some further clarification, if you found the time.

I still don’t quite get how the layer or the drawing sizes itself in the face of those missing bounds. Doesn’t the drawing need the layer’s bounds to be displayed at all (which, as I understood it, relates to the purpose of a CAScrollLayer and its clipsToBounds property)? Before starting with this course, I read the corresponding chapters in Matt Neuburg’s “Programming iOS 13” and in this context he explicitly warns:

“A layer created in code (as opposed to a view’s underlying layer) has a frame and bounds of CGRect.zero and will not be visible on the screen even when you add it to a superlayer that is on the screen. Be sure to give your layer a nonzero width and height if you want to be able to see it! Creating a layer and adding it to a superlayer and then wondering why it isn’t appearing is a common beginner error”

Regarding the above quote, doesn’t draw(_:) draw into the view’s main layer and thus omitting it would, as you explained, leave the main layer’s content to be nil?

You can always do view.layer.contents = image.cgImage (or whatever you want to put into that layer. There’s nothing special about the view’s layer. Only that the view always has a layer.

When you assign to a layer’s contents, you’re effectively saying “this is what I want the layer to show”. But if you want to build up various effects into one layer, that is where you would use draw(_:), which will draw into the view’s layer. For example, if you’re drawing a hot dog, which has a sausage, a bun and a squiggle of mustard. You can make several shape layers with paths, or you can draw all three paths into one CGContext. You can use a UIGraphicsImageRenderer context and save the image from there, or you can use draw(_ :)'s context to draw your hot dog straight into your view. If you save the image, that’s a bitmap, and if you resize it, it will become blurry.
But if you were to draw the paths to separate shape layers, then resizing the shape layers won’t become blurry.

If you save the hot dog to an image, then you can assign that image to a layer’s contents. But you can’t build up a bitmap into a layer’s contents in the same way as you can in a CGContext.

(I hope I’m not being too obtuse about understanding where your difficulties lie!)

thermoLayer 's bounds are always zero. You can print them to see. The path is being drawn outside of the layer’s bounds. You wouldn’t be able to perform a hit test on this layer.

Matt Neuberg is wonderful, and always right! However, CAShapeLayers are different from other layers, and their bounds can be zero. The paths that form their shape will draw outside the bounds.

Wonderful, thank you! That cleared things up a lot (and now I’m hungry thanks to your hot dog example).

I’ve got one last question, if that’s okay,: I get that prerendering our hot dog using a UIGraphicsImageRenderer would give us a fixed-size image that gets blurred upon resizing. But wouldn’t, given that we’re drawing the hot dog image into draw(_:), upon resizing the view draw(_:) be called again and cause the image to be rendered again with the new size?

Yes, you’re correct on both counts. UIGraphicsImageRenderer has a fixed size, and if you resize the view or call setNeedsDisplay(), then draw(_:) will redraw the image with the new size.

:hotdog:

Great. Once again, thank you very much for your help!