setNeedsDisplay And drawRect Explained

In my previous post, I explained the difference between setNeedsLayout and layoutIfNeeded. This post covers another related and important method of UIView, which is setNeedsDisplay.

29 March 2017: Please note that I’ve updated the project in GitHub for Swift 3.

If you read my previous post, it explained the three phases of the layout process, which are constraints, layout, and drawing (display). setNeedsDisplay is of course part of the latter.

To get started, in a Single View Application, I defined a subclass of UIView which will be used to demonstrate the use of setNeedsDisplay. This CircleView subclass will simply draw a circle within the drawRect method. It has a color property which is what we’ll be setting to change the color of the circle.

Note: I won’t discuss setNeedsDisplayInRect in this post, but it is the equivalent of setNeedsDisplay, except it specifies a specific rectangular region of a view as needing a display update, versus an entire view.

The next part of the app is configuring some UIKit components in the Storyboard, one of those being the CircleView. The CircleView is transparent, and is achieved by dragging a regular UIView to the Storyboard and then changing the Custom Class > Class value to CircleView in the Identity Inspector.

To allow a user to change the color, I’ve defined three UIStepper controls, with corresponding value labels (just for display) and also labels to indicate that the steppers correspond to RGB values. I’ve also added a button which will cause the stepper values to be used to construct a UIColor for setting the color property of the CircleView. I won’t go into much detail on how to configure the Storyboard, but using UIStackView is perfect for this situation as you can see if you look at the setup in the Storyboard. The net result is shown below.

Screen Shot 2016-04-13 at 4.24.38 PM

The next aspect to get setup is the ViewController. Required IBOutlet and IBAction connections are needed.

These are quite obvious hopefully. IBOutlets give us access to the circleView, the three steppers, and three labels. There are IBActions for changes in stepper values, just to set the label displays so you know what the stepper values are. Finally, there is a buttonPressed which will call an as of yet undefined method setColorFromSteppers method. That method will collect the values of the three steppers, create a UIColor with it, and set the color property of the circleView. Here is that method defined, along with the viewDidLoad.

In viewDidLoad, I’ve triggered an initial set of the circle color based on the default values of the steppers as configured in the Storyboard. The setColorFromSteppers method creates CGFloat values for the stepper numbers, creates the UIColor, and then sets circleView.color.

If you run this now, change values of the steppers, and click the Update Color button, you would see that the color does not update. Let’s talk about why that is, and then we’ll fix it.

In general, with framework controls you are used to using, when you set a property, such as the display label or a value, this causes the control to be redrawn because the system implements a call to the controls drawRect method. Because we’ve defined our own subclass of UIView, we need to handle updates to the control that affect the display. In the case of changing the color, of course that means the control needs to be redrawn.

Before we go further, there are three important principles to consider.

  1. Caching of views is used significantly by iOS, and often times, a given view may be drawn once and not need to be updated.
  2. Even in cases where a view might be moved, or have another view overlapping it, it may not need to be redrawn, so you can’t simply rely on having moved the whole view or added another view to result in a redraw based on a setNeedsLayout or updateIfNeeded.
  3. When writing a UIView subclass that overrides drawRect, you need to signify to the system when a redraw is needed. You should never call drawRect yourself. Instead, you tell the system that drawing needs to be done by using the setNeedsDisplay method, which marks the view as dirty. As discussed in the previous post, the drawRect method of our subclass would then be called during the next update cycle.

One reason I mentioned those three points is because even if we added a call to setNeedsLayout or layoutIfNeeded after circleView.color = color, the display would not be updated. Similarly, rotating the device would also not trigger a redraw of the circle. This is due to caching of the circle view, so that even when a layout pass takes place, and because the view isn’t dirty, it will just be redrawn as cached. setNeedsDisplay is needed to explicitly tell the system that it must be redrawn, and thus the new color is displayed.

Note: A reader (Prashant) kindly pointed out in the comments, that setNeedsLayout can also trigger a redraw when the UIViewContentMode == UIViewContentModeRedraw. Indeed you can see in the Apple documentation here that UIViewContentModeRedraw gives the option to redisplay the view when the bounds change by invoking the setNeedsDisplay method. Thanks to Prashant for contributing!

The updated setColorFromSteppers method would now simply be as follows.

There is another way to accomplish this without having to add the setNeedsDisplay into the ViewController.

In Swift, you can define a property observer with didSet to execute code when the property has been set. For the color being set in the CircleView class, here is the new definition of the color property that includes the property observer.

Since we are now calling setNeedsDisplay right from within the CircleView class, the method call in ViewController isn’t required anymore, so it is back to the original definition. For clarity, I’ve just commented out the setNeedsDisplay that was in the ViewController class.

The code for this project is in GitHub.

Leave a Comment:

Add Your Reply