How to "stick" a UIScrollView subview to top/bottom when scrolling? - uiscrollview

You see this in iPhone apps like Gilt. The user scrolls a view, and a subview apparently "sticks" to one edges as the rest of the scrollView slides underneath. That is, there is a text box (or whatever) in the scrollView, that as the scrollView hits the top of the view, then "sticks" there as the rest of the view continues to slide.
So, there are several issues. First, one can determine via "scrollViewDidScroll:" (during normal scrolling) when the view of interest is passing (or re-appearing). There is a fair amount of granularity here - the differences between delegate calls can be a hundred of points or more. That said, when you see the view approach the top of the scrollView, you turn on a second copy of the view statically displayed under the scrollView top. I have not coded this, but it seems like it will lack a real "stick" look - the view will first disappear then reappear.
Second, if one does a setContentOffset:animated, one does not get the delegate messages (Gilt does not do this). So, how do you get the callbacks in this case? Do you use KVO on "scroll.layer.presentationLayer.bounds" ?

Well, I found one way to do this. When the user scrolls by flicking and dragging, the UIScrollView gives its delegate a "scrollViewDidScroll:" message. You can look then to see if the scroller has moved the content to where you need to take some action.
When "sticking" the view, remove it from the scrollView, and then add it to the scrollView's superview (with an origin of 0,0). When unsticking, do the converse.
If you use the UIScrollView setContentOffset:animated:, it gets trickier. What I did was to subclass UIScrollView, use a flag to specify it was setContentOffset moving the offset, then start a fast running timer to monitor contentOffset.
I put the method that handles the math and sticking/unsticking the child view into this subclass. It looks pretty good.

Gilt uses a table view to accomplish this. Specifically, in the table view's delegate, these two methods:
– tableView:viewForHeaderInSection:
and – tableView:heightForHeaderInSection:

Related

Make sure NSTrackingArea refreshed in Nested NSScrollViews

I have an NSCollectionView in an NSScrollView.
The scroll view scrolls horizontally to move along the line of items.
Inside each each collection item is a vertically scrolling NSOutlineView.
I have NSButton objects for opening and closing collection items - as supplementary views in my collection.
I set up NSTrackingAreas on these buttons to support mouse over effects.
This works correctly, until I scroll, at which point the NSTrackingArea areas are clearly left behind (the mouse over effects happen when the mouse is where the button was, not where it is).
I rebuild my tracking areas in updateTrackingAreas in my button class, and this is called, but not often enough.
I have tried using .inVisibleRect when setting up my tracking areas, rather than explicitly rebuilding them, but that doesn't improve the tracking update.
I have tried calling updateTrackingAreas on the NSCollectionView when scroll occurs, but it is never passed down to the child views as I expected it would.
As a side note, I also have NSTextViews in my collection view items with toolTips that are very flakey too. They are often left hanging. Pointing hand cursors over links are often misaligned.
It feels as though the default updateTrackingArea is over-optimised and is not being called as often as it should.
So, I am about to embark on building my own tracking-area-tracker to register and update my views when they are not updated by default...
...but maybe someone can see something obvious that I am missing? Thank you.
The tracking area setup can be subtly broken when nesting scroll views incorrectly. See the answers regarding nesting scroll views here for details:
NSScrollView inside another NSScrollView

How to call NSScrollView autoscroll-method programmatically

I have simple chat application with text messages view-based NSTableView as you can see at the picture below.
Each message contains NSTextView instance having height to fit all the text.
All I need is to start NSScrollView (which NSTableView-instance is enclosed by) autoscrolling while the user selecting text dragging mouse far enough. Unfortunately, autoscrolling doesn't appear. In case of dragging somewhere outside of the text views all succeed.
I tried to call autoscroll:-method directly by simply push NSEvent-instance from NSTextView-subclass "mouse dragged"-event (like in example from this article):
- (void)mouseDragged:(NSEvent *)event
{
[self.scrollView autoscroll:event];
}
As I've overrode all the mouse events and implemented all the text selecting, this method often invokes. But the autoscrolling doesn't seem to work.
UPDATE
I figured out that before calling -autoscroll:-method there must be -mouseDown: of the same object. But it breaks my text selecting mechanism. The point even not in being first responder, there must be nothing but the mouseDown:-method.
Normally, a text view is within a scroll view of its own. Even if that's big enough to show all of the text without scrolling, it's still there. A call of -autoscroll: on anything within that scroll view (possibly including that scroll view itself?) will just try to scroll that scroll view, not the scroll view that contains the table view.
Try calling -autoscroll: on a view higher up in the hierarchy. Either self.scrollView.superview, the table cell view, or the table view.
Note, though, that the table view's scroll view will keep scrolling even after the cell view containing the text view is fully on-screen. In fact, it may keep scrolling it so far that it's off the screen in the other direction. Basically, it doesn't know that you're trying to select within the text view so it doesn't know to stop when the selection extends all the way to the edge of the text view.
Another approach might be to try to use a "bare" text view with no enclosing scroll view. I don't think IB will let you do that, so you'd have to do it programmatically. Bare text views don't play well with auto layout, though.

isOpaque not stopping passing to parents drawRect

I got a problem with Cocoa and its View redraw hierarchy.
I'm currently testing displaying (audio) levels in a meter style control and I'm using the MeteringView class from MatrixMixerTest example project from apple. This class is drawing the meter and only drawing the difference what got changed which looks like a very efficient class.
My project is splitted into 2 splitviews, in some are NSCollectionViews (Scrollview, Clipview) and in others are only static views. If I add the meter to those "static" views they work fine when these views call setNeedsDisplay:YES. If a meter is added to the view of a CollectionView Item it gets rendered, but loosing its drawn "old level" parts and its corners/background. I think this happens because the CollectionView item gets also called to be redrawn (which has a background image) and everything is gone. It is drawing some parts whats currently changing (the drawing works).
Is there a way to prevent the Item itself to be redrawn? Or, I dont know why it is not happening in those static views, because those views also have background images but do not draw over the meter.
Are there some tricks or whats different in a CollectionView than in a "normal" view?
EDIT: After reading about isOpaque (MeteringView isOpaque = YES) means it should not call the parent views drawRect if set to yes. Well that works for the static views, those MeteringViews do not call parents drawRect, but those in a CollectionView do however. I dont know why.
EDIT 2: I gave this topic another title, because isOpaque=YES in MeteringView is not stopping calling the parents drawRect in a CollectionView, in a normal view it is working. Are there some things to know about? I have to stop redrawing the CollectionView Item because thats the problem.
Thanks in advance guys
Benjamin
isOpaque is just hint to the system. It does not prevent other views from drawing their contents, it only means that it can sometimes skip making other views update their contents.
If your view is opaque, it should draw itself as opaque and completely fill its bounds.

How do I do something after the next NSView -layout has occurred?

In response to a user event, I want to:
add a new NSView to the window, and then
show an NSPanel positioned just below that view
I have each half of this done. I can add a new subview, and the container view's -updateConstraints identifies it and adds the correct layout constraints, so that the next time layout is performed, it's positioned correctly in the window. Also, I have a NSWindowController subclass that puts the panel on the screen.
Unfortunately, there's an ordering problem. My panel's controller just looks at the new NSView's frame property for deciding where to put it, but during this iteration of the main event loop, the -layout method hasn't been called yet, so it's still positioned at (0,0).
(If I separate these two pieces of functionality, and require two separate user events for "add view" and "create panel", then the panel is correctly positioned below the view.)
Is there a way to attach an NSPanel to an NSView, as if with a layout constraint? Or is there a way to say "do this (window controller stuff), but only after the next -layout call"?
Just call -layoutSubtreeIfNeeded on your NSView’s superview as soon as you add it and its constraints, so it will lay out immediately, then add the panel.
Or use an NSPopOver, although those draw a certain way and you might not want that.

Limiting scroll elasticity in Cocoa (NSScrollView)

I have a cocoa application that has a dozen scrollViews. I love the elasticity, especially in some cases where I'd actually put some kind of "Easter egg" (kinda like the apple logo in the books app. you scroll down, you see an apple logo.)
My problem is, that I need to limit the amount of exposed content beyond the actual content area. When I scroll with the magic mouse, especially, the elasticity causes the whole scroll content to disappear! Until you release the scroll, it moves back in.
Now, I would like to limit the elasticity to a specific margin. how?
NSScrollView manages a view which has a "canvas" bigger than what is/can be display at any one time. So if you want a different behaviour:
Check (void)setHorizontalScrollElasticity: but that doesn't quite do what you want. (you want to allow a fixed amount of elasticity)
Subclass NSScrollView to implement the behaviour you want.
Create your own class from scratch (well... Anything inheriting from NSResponder since you want to handle events).
For example, I once wrote a world-map program but needed the map to loop forever on the horizontal axis. I just manually managed the scrolling with a subclassed NSView. (don't have access to code currently)
Something to ponder about: I understand your reasons but just wanted to mention it. The behaviour should be expected by the user. If it looks like a button, it should act like out. Currently, scrollviews have the elasticity so that when they scroll via momentum (user is no longer touching), it doesn't stop suddenly once it reaches the end... which would be jarring for users.
Example
If subclassing NSScrollview, I would try overriding - (void)scrollWheel:(NSEvent *) and detect what are the bounds of the contentView and cap it at a certain value. Something around the lines of:
- (void)scrollWheel:(NSEvent *)event
{
[super scrollWheel:event];
if (self.contentView.bounds.origin.y > SomeConstant)
/* cap the value */
}

Resources