

Loren Brichter may h ave invented “pull to refresh”, but how about “push to dig out archives?” In a scrolling list view of items in reverse-chronological order, if pulling the scroll view downwards expresses the user’s desire to get newer items, then it’s natural that “pushing” past the end of the list indicates that the user wants to load older items.

In this post, you’ll learn how to extend OS X’s scroll view to add both “pull to refresh” and “push to load archives” – that is pulling to refresh in the other direction is taken as a gesture to load older data into the scroll view. I did this as part of Scuttlebutt – our upcoming Yammer client for OS X. Stick around until the end of the post and you can find sources that you can use in your own projects. Hopefully Mr Brichter won’t sue us ^_^

The Design

We start with a plain old NSScrollView class and then add progress indicator views at the top and at the bottom of the scrollable document area. That is, when the user scrolls past the top or bottom edges of the document view then the scroll view will trigger refresh for that area. It’s the classic pull-to-refresh behavior that you see in iOS’ Mail app but also applied to the bottom edge of the scroll view to load older data.

To prevent accidental refreshes, refresh is only triggered after all of the progress indicator view is brought into the visible area – in other words when it’s only partially shown then the refresh cycle won’t be started. After the refresh is triggered and while it is still running (waiting for network I/O for example), the progress indicator will remain visible and part of the scrollable area. When refresh completes, then the scroll view snaps back into place and again hides the refresh indicator(s).

The Details

How to implement this

Subclass NSScrollView

Add the top and bottom refresh views which hosts the progress indicators as well as any other controls you would like to display there.

Override contentView to use your own custom subclass of the content view.

to use your own custom subclass of the content view. Override scrollWheel: to detect when the user scrolled pass the top or bottom edges and handle them appropriately.

to detect when the user scrolled pass the top or bottom edges and handle them appropriately. Add a delegate for the scroll view to handle the actual refresh logic.

Subclass NSClipView

This is the custom content view that is used by your scroll view subclass and is tightly coupled to it.

Override constrainScrollPoint: to allow scrolling past the edges of the document view if a refresh is currently in progress for those edges.

to allow scrolling past the edges of the document view if a refresh is currently in progress for those edges. Override documentRect to extend the scrollable area to include the top and bottom progress indicator views if a refresh is in progress.

to extend the scrollable area to include the top and bottom progress indicator views if a refresh is in progress. Calculating the clip view’s coordinates in the two overridden methods above will be easier to do in flipped coordinates, so override isFlipped to return true.

The Code

I’ve released the code for this dual-direction scroll-to-refresh as an open-source project on Github with a liberal BSD license. Also included is an Xcode project that builds a static library that you can include in your own projects and an example application that shows you on how to use it.

The main class is BSRefreshableScrollView which is a drop-in replacement for NSScrollView . By default it will behave exactly like plain old scroll view until you enable it’s pull-to-refresh behaviour. Set the refreshableSides property to enable the top or bottom edges to trigger refreshes – it’s a bitmask so you can set both edges to be refreshable. You can even disable refresh edges while the scroll view is still active if you’re not allowing refresh for that particular time.

Then create a delegate for the scroll view and implement the scrollView: startRefreshSide: method. In the implementation, you kickoff the refresh process and return YES if it’s started or NO if the operation could not be started. The scroll view will then display the progress indicator at the appropriate edge. When refresh completes, call the scroll view’s stopRefreshingSide: method to hide the progress indicator.

Checkout the video below to see it in action.

That’s all for now. Till next time.