NSScrollView and copiesOnScroll behavior

Hi all,


(

Note that this is the 4th time attempting to submit this post and get by the dreaded:

"Please note: your content was added successfully, but a moderator needs to approve it before it can be posted"

)


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 as seen in the stack (but not copying here because I am trying to figure out what is not allowing this post to be posted).


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 (but not copying here because I am trying to figure out what is not allowing this post to be posted)


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.


Thanks,
Chris

>> Now let’s scroll everything to the left by 500


How are you doing that? Programmatically? Specifically, where does the number 500 come from?


>>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


Assuming this means what it looks like it means (it could, after all, be some internal artifact of the implementation of the NSClipView), you only scrolled 180.5 points, not 500. That's consistent with the call to MyCustomView's drawRect that you actually saw.


BTW, does MyCustomView have any subviews (including controls)?


For debugging purposes, you could try something like having your drawRect start by filling the dirtyRect with a random color. That way, it would be much more obvious what was being copied (if anything) and what was being re-drawn — and in how many separate calls.

>> How are you doing that? Programmatically? Specifically, where does the number 500 come from?


Sorry about the confusion here. The 500 in the introductory paragraph was just to lay out a simple example with easy numbers. Later on, I was actually debugging the process, where I said "When clicking in the scroll bar to start scrolling to the right". I also have code to programmatically do a scrollPoint by 100 in my mouseDown:


[self scrollPoint:NSMakePoint(self.visibleRect.origin.x + 100, self.visibleRect.origin.y)];


and it shows the same behavior as clicking in the scroll bar.


>>BTW, does MyCustomView have any subviews (including controls)?


No.


>>For debugging purposes, you could try something like having your drawRect start by filling the dirtyRect with a random color. That way, it would be much more obvious what was being copied (if anything) and what was being re-drawn — and in how many separate calls.


Been doing exactly that. The dirtyRect always looks like the entire visibleRect. After much frustration I turned to the forums 🙂

Well, unfortunately, I don't think that the "copiesOnScroll" property is an API contract that guarantees it will always be done. It may also be affected by other things, such as this (from NSClipVIew documentation):


It is also important to note that setting drawsBackground to false in an NSScrollView has the added effect of setting the NSClipView property copiesOnScroll to false.


but there could also be other environmental view settings that suppress the coping. I suspect the only way to guarantee the behavior is to draw your content into a layer.

>> that guarantees it will always be done.


I've yet to see a case where it EVER works (in recent macOS releases like 10.13, pretty sure it worked once upon a time).


>>It is also important to note that setting drawsBackground to false in an NSScrollView has the added effect of setting the NSClipView property copiesOnScroll to false.


I had already verified we weren't doing this. You can actually see IB (currently in Xcode 9.4.1) uncheck "Copy On Scroll" when you uncheck "Draw Background".


Also did a little more searching on the cocoa-dev list and saw someone else reporting the same issue. That user stated:


"We think copiesOnScroll behavior of NSClipView has changed in OSX 10.10?. Because while scrolling it is not asking for redraw of newly exposed area!!! Instead is asking for entire visible area to be redrawn every time."


Might be time to file a bug and try to get more info from Apple.

>> Might be time to file a bug and try to get more info from Apple.


Excellent idea, especially since you have this isolated in a sample project.


>> Also did a little more searching on the cocoa-dev list and saw someone else reporting the same issue.


Hmm. This tickled my memory a little bit. My recollection isn't clear, and I may be confusing different issues, but there was something once where expected NSView behavior was suppressed because the view had its "wantsLayer" flag set in IB. This is in the last inspector panel ("View Effects"): the checkbox at the top under "Core Animation Layer". If this is set for your document view, or maybe the scroll or clip view, try unchecking it.


It used to be that this checkbox was never checked by default, but in recent Xcode releases it does seem to start out checked.

>>but there was something once where expected NSView behavior was suppressed because the view had its "wantsLayer" flag set in IB


Good suggestion, and just noting that I had also previously confirmed the wantsLayer stuff was off. I was definitely not ready to pull in all the CALayer machinery (yet).


And just wanted to add some extra information that this definitely broke sometime between 10.7 and 10.13 (my suspicion would be around 10.9 or 10.10, certainly with all the changes for responsive scrolling in 10.9). I had an older developement machine with 10.7.5 and Xcode 4.6.3 and just re-did the test project again, and the dirtyRect is working as I expected (and as documented) along with the content currently visible being, well, "copied on scroll". In the image below (uh, ascii-art, no way yet to add images to posts?), imagine it is the visible content of the view (all gray) and as we scroll right to reveal more content to the right, at each drawRect call, I alternate (between black and gray in the art work below) coloring the dirty rect. I of course get the familiar looking picture like this:


⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜⬛⬜⬛⬜⬜⬛⬜⬜⬛⬛⬜⬛⬜⬛

Have you tried copiesOnScroll on Mojave? I use it extensivelly and it stopped working on OSX 10.14

NSScrollView and copiesOnScroll behavior
 
 
Q