I am trying to understand the behavior of copiesOnScroll in a typical NSScrollView/NSClipView/NSView setup. I’ll refer to the NSView or documentView as MyCustomView throughout, and just note that this is a large view that can take substantial time to draw (I am actually doing this in a sample app to better understand all the various issues at play here). Note also that I am currently working on the “non-layer backed case” and not opting in to responsive scrolling. Let’s say the view is 10,000 x 800 units. Assume also that this scroll view takes up the entire window, and we are simply viewing the beginning portion of MyCustomView in a window that is 1,500 x 800 (so we are viewing the rect (l: 0, t:0, r:1500, b:800)). Now let’s scroll everything to the left by 500 so that we are now viewing (l:500, t:0, r:2000, b:800). During such a scroll, I would expect, with copiesOnScroll set to YES, that the dirty rect we get in MyCustomView’s drawRect method would be (l: 1500, t:0, r:2000, b:800) but instead we get the entire visible area: (l:500: t:0, r:2000, b:800).
According to Apple docu on copiesOnScroll:
When the value of this property is YES, the clip view copies its existing rendered image while scrolling (only drawing exposed portions of its document view); when it is NO, the view forces its contents to be redrawn each time.
For what it’s worth, this was the behavior we saw with scrolling when our app (the real app, not this sample one) used to be a Carbon based app, the update region would only consist of the newly exposed parts of the visible region.
In order to understand this behavior a little more, I substituted MyScrollView/MyClipView subclasses for the NSScrollView/NSClipView part of the setup. Overriding a few routines and just calling super to get a feel for whose was calling various update routines (like setNeedsDisplay or setNeedsDisplayInRect:) I found the following:
When clicking in the scroll bar to start scrolling to the right, I get the following calls to MyClipView::setNeedsDisplayInRect:(NSRect)invalidRect:
invalidRect NSRect (origin = (x = 0, y = 0), size = (width = 0, height = 0))
invalidRect NSRect (origin = (x = 0, y = 0), size = (width = 0, height = 0))
And then:
invalidRect NSRect (origin = (x = 180.5, y = 0), size = (width = 460, height = 363))
This is the entire visibleRect being invalidated as a result of the translateOriginToPoint call apparently. And here is the stack:
0 0x0000000100001008 in -[MyClipView setNeedsDisplayInRect:] MyClipView.m:31
1 0x00007fff533ff139 in -[NSView setNeedsDisplay:] ()
2 0x0000000100000fe1 in -[MyClipView setNeedsDisplay:] MyClipView.m:26
3 0x00007fff534f97b4 in -[NSView translateOriginToPoint:] ()
4 0x00007fff538cd6eb in -[NSClipView _nonLayerBackedImmediateScrollToPoint:] ()
5 0x00007fff534f9405 in -[NSClipView _immediateScrollToPoint:] ()
6 0x00007fff538cecbc in -[NSScrollAnimationHelper _doAnimationStepWithProgress:] ()
7 0x00007fff538cebca in -[NSScrollAnimationHelper _doAnimationStep] ()
8 0x00007fff53830f8d in _NSAnimationHelperTimerCallback ()
9 0x00007fff55e8f064 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()
10 0x00007fff55e8ecd7 in __CFRunLoopDoTimer ()
11 0x00007fff55e8e7da in __CFRunLoopDoTimers ()
12 0x00007fff55e85dab in __CFRunLoopRun ()
13 0x00007fff55e851a3 in CFRunLoopRunSpecific ()
14 0x00007fff5516dd96 in RunCurrentEventLoopInMode ()
15 0x00007fff5516db06 in ReceiveNextEventCommon ()
16 0x00007fff5516d884 in _BlockUntilNextEventMatchingListInModeWithFilter ()
17 0x00007fff53420a73 in _DPSNextEvent ()
18 0x00007fff53bb6e34 in -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] ()
19 0x00007fff53b3adc7 in -[NSScroller trackPagingArea:] ()
20 0x00007fff53b3b2e7 in -[NSScroller mouseDown:] ()
21 0x00007fff53d57d6d in -[NSWindow(NSEventRouting) _handleMouseDownEvent:isDelayedEvent:] ()
22 0x00007fff53d549c4 in -[NSWindow(NSEventRouting) _reallySendEvent:isDelayedEvent:] ()
23 0x00007fff53d53c70 in -[NSWindow(NSEventRouting) sendEvent:] ()
24 0x00007fff53bb5236 in -[NSApplication(NSEvent) sendEvent:] ()
25 0x00007fff534158b5 in -[NSApplication run] ()
26 0x00007fff533e4a72 in NSApplicationMain ()
27 0x0000000100003a42 in main main.m:12
28 0x00007fff7e1a9015 in start ()
And then:
invalidRect NSRect (origin = (x = 460, y = 0), size = (width = 180.5, height = 363))
This looks like the correct newly “revealed” section to draw. And here is the stack:
0 0x0000000100001008 in -[MyClipView setNeedsDisplayInRect:] at MyClipView.m:31
1 0x00007fff538cdf90 in -[NSClipView _nonLayerBackedImmediateScrollToPoint:] ()
2 0x00007fff534f9405 in -[NSClipView _immediateScrollToPoint:] ()
3 0x00007fff538cecbc in -[NSScrollAnimationHelper _doAnimationStepWithProgress:] ()
4 0x00007fff538cebca in -[NSScrollAnimationHelper _doAnimationStep] ()
5 0x00007fff53830f8d in _NSAnimationHelperTimerCallback ()
6 0x00007fff55e8f064 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()
7 0x00007fff55e8ecd7 in __CFRunLoopDoTimer ()
8 0x00007fff55e8e7da in __CFRunLoopDoTimers ()
9 0x00007fff55e85dab in __CFRunLoopRun ()
10 0x00007fff55e851a3 in CFRunLoopRunSpecific ()
11 0x00007fff5516dd96 in RunCurrentEventLoopInMode ()
12 0x00007fff5516da0f in ReceiveNextEventCommon ()
13 0x00007fff5516d884 in _BlockUntilNextEventMatchingListInModeWithFilter ()
14 0x00007fff53420a73 in _DPSNextEvent ()
15 0x00007fff53bb6e34 in -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] ()
16 0x00007fff53415885 in -[NSApplication run] ()
17 0x00007fff533e4a72 in NSApplicationMain ()
18 0x0000000100003a42 in main at main.m:12
19 0x00007fff7e1a9015 in start ()
And then 3 more of these:
invalidRect NSRect (origin = (x = 0, y = 0), size = (width = 0, height = 0))
invalidRect NSRect (origin = (x = 0, y = 0), size = (width = 0, height = 0))
invalidRect NSRect (origin = (x = 0, y = 0), size = (width = 0, height = 0))
And eventually the MyCustomView drawRect is called with the dirtyRect: dirtyRect NSRect (origin = (x = 180.5, y = 0), size = (width = 460, height = 363)) instead of the desired: dirtyRect NSRect (origin = (x = 460, y = 0), size = (width = 180.5, height = 363))
So any idea why copiesOnScroll doesn’t send an appropriately smaller dirtyRect (or a list of smaller rects via getRectsBeingDrawn)? This extra drawing is causing significant performance issues in the real app.