I'm working on an app where the user can toggle individual squares on and off in a grid that can potentially be hundreds of squares in each direction. I have a custom view that is drawing all the squares and is inside a UIScrollView
's content view. When the user taps a square, it's state changes, and I call a function on my view that computes the rectangle of dirty pixels that need to be redrawn, and calls [self setNeedsDisplayInRect:dirtyRect]
accordingly.
However, the next call to my view's drawRect:
function is given a rect that is the same as the bounds of the view (which can be something like thousands of pixels in each direction). This causes a noticeable slowdown since we end up redrawing the entire view (potentially millions of pixels) instead of a single square that's less than a few hundred pixels.
To do a little digging and make sure I wasn't shooting myself in the foot somewhere, I overrode setNeedsDisplay
and setNeedsDisplayInRect:
so that I could print out the dirty rectangle and know when each was getting called:
- (void)setNeedsDisplayInRect:(CGRect)rect
{
NSLog(@"setNeedsDisplayInRect: %f %f %f %f", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
[super setNeedsDisplayInRect:rect];
}
- (void)setNeedsDisplay
{
NSLog(@"setNeedsDisplay");
[super setNeedsDisplay];
}
When I run my app, and tap on a square, I get the following logs:
2019-08-03 15:39:28.763032-0500 MyCoolApp[4923:4191214] done loading
2019-08-03 15:39:28.765756-0500 MyCoolApp[4923:4191214] setNeedsDisplay
2019-08-03 15:39:28.765851-0500 MyCoolApp[4923:4191214] setNeedsDisplay
2019-08-03 15:39:28.765995-0500 MyCoolApp[4923:4191214] setNeedsDisplay
2019-08-03 15:39:28.775883-0500 MyCoolApp[4923:4191214] setNeedsDisplay
2019-08-03 15:39:28.812023-0500 MyCoolApp[4923:4191214] drawRect: 0.000000 0.000000 4533.000000 5411.000000 // this is the initial draw for the full view as expected
2019-08-03 15:39:38.220319-0500 MyCoolApp[4923:4191214] processing toggle at row=371 col=337
2019-08-03 15:39:38.220890-0500 MyCoolApp[4923:4191214] setNeedsDisplayInRect: 2056.000000 2264.000000 6.000000 6.000000
2019-08-03 15:39:38.245801-0500 MyCoolApp[4923:4191214] drawRect: 0.000000 0.000000 4533.000000 5411.000000 // why is this the full view rect?
The first section of logs is from the view initializing and doing its first draw, and looks just fine. The second section is from me tapping a square to toggle it:
- we start processing the toggle action
- we set the dirty rect to the appropriate pixels
- we redraw
I'm curious why the dirty rectangle I'm being given is the full bounds of the view. Is it something to do with the fact that it's in a UIScrollView
? My instincts say no since that should have been something that was optimized a long time ago.
Is there some way I can get access to the list of invalid/dirty rectangles in the view or put a breakpoint on it so I can see where it's getting updated?
Is there some fundamental way that things work that I'm just missing?
Somewhat separate question:
Is it okay to track my own internal dirty rects and only draw those in drawRect:
regardless of what the passed in rectangle is, or is there no guarantee that my content in those areas may or may not already be drawn, or may have been cleared out?
UPDATE:
I've also done a bit of investigation into what happens if I simply choose not to draw the whole given dirty rect, and it turns out that the system is clearing the context after every draw! In this case it makes sense why I would be getting a rectangle different from my set dirty rect.
To try and prevent this, I tried setting self.clearsContextBeforeDrawing = NO;
, but that didn't do anything and the view still clears out (I have the backgroundColor
set to UIColor.clearColor
). Is my understanding of that variable wrong? Given that changing the variable didn't help, I'm betting that whether or not the view is cleared out isn't really the issue here.