Friday, January 18, 2008

Travail with scrolling

Had quite the frustrating time with a seemingly simple UI concept over the last few days.

I have some UI (a view hierarchy) consisting of a scrolling single column that shows one tile at each row, but where the row can scroll left/right. These horizontal 'strips' of tiles change depending on the application state, and the number of tiles in a strip typically grows, with the right-most strip representing a 'current object' and all the tiles to the left representing 'historical objects'. What I want to do is scroll the strip to the right-most object (the current one) whenever a new tile appears in the strip.

Now, all the Cocoa binding magic works beautifully, and so does the view hierarchy. The root view is an NSScrollView containing an NSCollectionView set up to have one column and zero rows (meaning "don't care"). The views that populate this column are the 'rows', represented by an NSScrollView containing an NSCollectionView set up to have one row and zero columns. Indicating that a certain area of a particular subview should be scrolled-to by the nearest antecedent NSScollView is trivial: any view can call - scrollRectToVisible: with an area in its own coordinate space, and this message is passed up the view hierarchy to the nearest NSScrollView which attempts to comply. Rather nice. So, I went looking for the where/when I could introduce such a call on my view.

Finding such a place was a little challenging, partly because of what I was trying to do in my code, but partly also because I was looking for a message that gets sent to a view when it has been newly added to a superview and everything has settled down (i.e. the resizing of the NSCollectionView has happened, so the area being requested of the scroller actually exists in its notion of the bounds of the document view). Originally, I thought that -didMoveToSuperview would be a good message, as this might indicate that my new tile was fully integrated into the superview's notion of the world. However, it appears that this message is sent out quite early, and the superview (the NSCollectionView in this case) hasn't had a chance to resize yet.

Then, on advice from the CocoaDev mailing list, I attempted to hook into the frame sizing of the NSCollectionView itself, and use events about it resizing to a scroll to the last tile. The manner in which you have to register with the NSNotificationCenter to get information about your own frame changes seemed a little odd when I approached an implementation. At the moment (!) I don't understand why a view can't override a method to be told about frame and bounds changes, but perhaps there's a reason for having to go about this the 'long way' (maybe the NextStep/Cocoa designers thought they already had a general purpose advisory concept for all, and that should also be what is used for the 'local view'). Anyway, the number of notifications of frame change I'm getting here on an addition of a tile is huge, and that this leads me to wonder whether there's something wrong with my code (more about this in a second...). In the end, I opted for each extant tile view to register for its own frame notifications (assuming that the tile view must be positioned within the NSCollectionView at some point, by definition setting its frame's location). On notification, I test to see if this tile is the 'last tile' and if so, I request a scroll to the bounds of the tile. This seems to work except...

I noticed that the result of the scroll was that the strip was positioned one pixel (or something) too far right, so I could see a tiny portion of the tile to the left of the last one. This looked pretty awful and let me to further wonder whether I still hadn't found the correct place to put my scrolling request. Nevertheless, the scrolling was almost happening right, and I had rechecked that the requested bounds were indeed the current bounds of the tile. Mmmm.

The big question was (assuming I _was_ basically doing the scrolling at the right time): what could cause a rectangle to be incorrect by a small amount (between when I had sampled it from [myTile bounds], and how this would eventually be positioned within the coordinate space of the superview. I resorted to recheck my NIB and here's what I found was that I had some 'Autoresizes subviews' set on some of the views above the tile in the eventual view hierarchy (namely the NSCollectionView representing the strip, and the NSScrollView above this). By turning off the resize flag on the NSCollectionView (figuring tiles in an NSCollectionView are _never_ generally resized), the 'out by one error' disappeared. Flushed with success, I resorted to turning off the "Resizes subviews" flag on the NSScrollView above this, BUT THE PROBLEM REAPPEARED.

At this point, I have fathomed a working set of conditions to get the scrolling to do what I want, but I'm still bothered by a sense that I'm not sure when view hierarchies are supposed to have 'settled down' after the perturbation of having a deep subview added. I clearly need to know when such a point has been attained, and its safe to request a scroll in a coordinate space way down the hierarchy that is highly dependent on all the transforms up the tree.

No doubt I'll work up the energy to research this further when my currently working code decides to be sensitive to some new condition of the application in the future...

No comments: