iOS: Using UIView's 'drawRect:' vs. it

2019-01-12 16:05发布

I have a class which is a subclass of UIView. I am able to draw stuff inside the view either by implementing the drawRect method, or by implementing drawLayer:inContext: which is a delegate method of CALayer.

I have two questions:

  1. How to decide which approach to use? Is there a use case for each one?
  2. If I implement drawLayer:inContext:, it is called (and drawRect isn't, at least as far as putting a breakpoint can tell), even if I don't assign my view as the CALayer delegate by using:

    [[self layer] setDelegate:self];

    how come the delegate method is called if my instance is not defined to be the layer's delegate? and what mechanism prevents drawRect from being called if drawLayer:inContext: is called?

6条回答
别忘想泡老子
2楼-- · 2019-01-12 16:31

On iOS, the overlap between a view and its layer is very large. By default, the view is the delegate of its layer and implements the layer's drawLayer:inContext: method. As I understand it, drawRect: and drawLayer:inContext: are more or less equivalent in this case. Possibly, the default implementation of drawLayer:inContext: calls drawRect:, or drawRect: is only called if drawLayer:inContext: is not implemented by your subclass.

How to decide which approach to use? Is there a use case for each one?

It doesn't really matter. To follow the convention, I would normally use drawRect: and reserve the use of drawLayer:inContext: when I actually have to draw custom sublayers that are not part of a view.

查看更多
Lonely孤独者°
3楼-- · 2019-01-12 16:34

Whether you use drawLayer(_:inContext:) or drawRect(_:) (or both) for custom drawing code depends on whether you need access to the current value of a layer property while it is being animated.

I was struggling today with various rendering issues related to these two functions when implementing my own Label class. After checking out the documentation, doing some trial-and-error, decompiling UIKit and inspecting Apple's Custom Animatable Properties example I got a good sense on how it's working.

drawRect(_:)

If you don't need to access the current value of a layer/view property during its animation you can simply use drawRect(_:) to perform your custom drawing. Everything will work just fine.

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

drawLayer(_:inContext:)

Let's say for example you want to use backgroundColor in your custom drawing code:

override func drawRect(rect: CGRect) {
    let colorForCustomDrawing = self.layer.backgroundColor
    // your custom drawing code
}

When you test your code you'll notice that backgroundColor does not return the correct (i.e. the current) value while an animation is in-flight. Instead it returns the final value (i.e. the value for when the animation is completed).

In order to get the current value during the animation, you must access the backgroundColor of the layer parameter passed to drawLayer(_:inContext:). And you must also draw to the context parameter.

It is very important to know that a view's self.layer and the layer parameter passed to drawLayer(_:inContext:) are not always the same layer! The latter might be a copy of the former with partial animations already applied to its properties. That way you can access correct property values of in-flight animations.

Now the drawing works as expected:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}

But there are two new issue: setNeedsDisplay() and several properties like backgroundColor and opaque do no longer work for your view. UIView does no longer forward calls and changes to its own layer.

setNeedsDisplay() does only do something if your view implements drawRect(_:). It doesn't matter if the function actually does something but UIKit uses it to determine whether you do custom drawing or not.

The properties likely don't work anymore because UIView's own implementation of drawLayer(_:inContext:) is no longer called.

So the solution is quite simple. Just call the superclass' implementation of drawLayer(_:inContext:) and implement an empty drawRect(_:):

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}

Summary

Use drawRect(_:) as long as you don't have the problem that properties return wrong values during an animation:

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

Use drawLayer(_:inContext:) and drawRect(_:) if you need to access the current value of view/layer properties while they are being animated:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}
查看更多
小情绪 Triste *
4楼-- · 2019-01-12 16:37

drawRect should only be implemented when absolutely needed. The default implementation of drawRect includes a number of smart optimizations, like intelligently caching the view's rendering. Overriding it circumvents all of those optimizations. That's bad. Using the layer drawing methods effectively will almost always outperform a custom drawRect. Apple uses a UIView as the delegate for a CALayer often - in fact, every UIView is the delegate of it's layer. You can see how to customize the layer drawing inside a UIView in several Apple samples including (at this time) ZoomingPDFViewer.

While the use of drawRect is common, it's a practice that has been discouraged since at least 2002/2003, IIRC. There aren't many good reasons left to go down that path.

Advanced Performance Optimization on iPhone OS (slide 15)

Core Animation Essentials

Understanding UIKit Rendering

Technical Q&A QA1708: Improving Image Drawing Performance on iOS

View Programming Guide: Optimizing View Drawing

查看更多
倾城 Initia
5楼-- · 2019-01-12 16:38

Here're codes of Sample ZoomingPDFViewer from Apple:

-(void)drawRect:(CGRect)r
{

    // UIView uses the existence of -drawRect: to determine if it should allow its CALayer
    // to be invalidated, which would then lead to the layer creating a backing store and
    // -drawLayer:inContext: being called.
    // By implementing an empty -drawRect: method, we allow UIKit to continue to implement
    // this logic, while doing our real drawing work inside of -drawLayer:inContext:

}

-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    ...
}
查看更多
不美不萌又怎样
6楼-- · 2019-01-12 16:40

How to decide which approach to use? Is there a use case for each one?

Always use drawRect:, and never use a UIView as the drawing delegate for any CALayer.

how come the delegate method is called if my instance is not defined to be the layer's delegate? and what mechanism prevents drawRect from being called if drawLayer:inContext: is called?

Every UIView instance is the drawing delegate for its backing CALayer. That's why [[self layer] setDelegate:self]; seemed to do nothing. It's redundant. The drawRect: method is effectively the drawing delegate method for the view's layer. Internally, UIView implements drawLayer:inContext: where it does some of its own stuff and then calls drawRect:. You can see it in the debugger:

drawRect: stacktrace

This is why drawRect: was never called when you implemented drawLayer:inContext:. It's also why you should never implement any of the CALayer drawing delegate methods in a custom UIView subclass. You should also never make any view the drawing delegate for another layer. That will cause all sorts of wackiness.

If you are implementing drawLayer:inContext: because you need to access the CGContextRef, you can get that from inside of your drawRect: by calling UIGraphicsGetCurrentContext().

查看更多
兄弟一词,经得起流年.
7楼-- · 2019-01-12 16:55

The Apple Documentation has this to say: "There are also other ways to provide a view’s content, such as setting the contents of the underlying layer directly, but overriding the drawRect: method is the most common technique."

But it doesn't go into any details, so that should be a clue: don't do it unless you really want to get your hands dirty.

The UIView's layer's delegate is pointed at the UIView. However, the UIView does behave differently depending on whether or not drawRect: is implemented. For example, if you set the properties on the layer directly (such as its background color or its corner radius), these values are overwritten if you have a drawRect: method - even if its completely empty (i.e. doesnt even call super).

查看更多
登录 后发表回答