Typically, the scrollView’s content view is a rectangle. But I would like to implement that is not a rectangle…. For example….

The yellow, Grid 6 is the current position…Here is the example flow:
- User swipe to left. (cannot scroll to left) Current: 6.
- User swipe to right. (scroll to right) Current: 7.
- User swipe to down. (scroll to down) Current: 8.
- User swipe to down. (cannot scroll to down) Current: 8.
As you can see, the Content view of the scrollView is not rectangle. Any ideas on how to implement it? Thanks.
This is an interesting idea to implement. I can think of a few approaches that might work. I tried out one, and you can find my implementation in my github repository here. Download it and try it out for yourself.
My approach is to use a normal
UIScrollView, and constrain itscontentOffsetin the delegate’sscrollViewDidScroll:method (and a few other delegate methods).Preliminaries
First, we’re going to need a constant for the page size:
And we’re going to need a data structure to hold the current x/y position in the grid of pages:
We need to declare that our view controller conforms to the
UIScrollViewDelegateprotocol:And we’re going to need instance variables to hold the grid (map) of pages, the current position in that grid, and the scroll view:
Initializing the map
My map is just an array of arrays, with a string name for each accessible page and
[NSNull null]at inaccessible grid positions. I’ll initialize the map from my view controller’s init method:Setting up the view hierarchy
My view hierarchy will look like this:
Normally I’d set up some of my views in a xib, but since it’s hard to show xibs in a stackoverflow answer, I’ll do it all in code. So in my
loadViewmethod, I first set up a “content view” that will live inside the scroll view. The content view will contain a subviews for each page:Then I’ll create my scroll view:
I add the content view as a subview of the scroll view and set up the scroll view’s content size and offset:
Finally, I create my top-level view and give it the scroll view as a subview:
Here’s how I compute the scroll view’s content offset for the current map position, and for any map position:
To create subviews of the content view for each accessible page, I loop over the map:
And here’s how I create each page view:
Constraining the scroll view’s
contentOffsetAs the user moves his finger around, I want to prevent the scroll view from showing an area of its content that doesn’t contain a page. Whenever the scroll view scrolls (by updating its
contentOffset), it sendsscrollViewDidScroll:to its delegate, so I can implementscrollViewDidScroll:to reset thecontentOffsetif it goes out of bounds:First, I want to constrain
contentOffsetso the user can only scroll horizontally or vertically, not diagonally:Next, I want to constrain
contentOffsetso that it only shows parts of the scroll view that contain pages:If my constraints modified
contentOffset, I need to tell the scroll view about it:Finally, I update my idea of the current map position based on the (constrained)
contentOffset:Here’s how I compute the map position for a given
contentOffset:Here’s how I constrain the movement to just horizontal or vertical and prevent diagonal movement:
Here’s how I constrain
contentOffsetto only go where there are pages:Deciding whether a point is accessible turns out to be the tricky bit. It’s not enough to just round the point’s coordinates to the nearest potential page center and see if that rounded point represents an actual page. That would, for example, let the user drag left/scroll right from page 1, revealing the empty space between pages 1 and 2, until page 1 is half off the screen. We need to round the point down and up to potential page centers, and see if both rounded points represent valid pages. Here’s how:
Checking whether a map position is accessible means checking that it’s in the bounds of the grid and that there’s actually a page at that position:
Forcing the scroll view to rest at page boundaries
If you don’t need to force the scroll view to rest at page boundaries, you can skip the rest of this. Everything I described above will work without the rest of this.
I tried setting
pagingEnabledon the scroll view to force it to come to rest at page boundaries, but it didn’t work reliably, so I have to enforce it by implementing more delegate methods.We’ll need a couple of utility functions. The first function just takes a
CGFloatand returns 1 if it’s positive and -1 otherwise:The second function takes a velocity. It returns 0 if the absolute value of the velocity is below a threshold. Otherwise, it returns the sign of the velocity:
Now I can implement one of the delegate methods that the scroll view calls when the user stops dragging. In this method, I set the
targetContentOffsetof the scroll view to the nearest page boundary in the direction that the user was scrolling:Here’s how I find the nearest page boundary in a horizontal direction. It relies on the
isAccessibleMapPosition:method, which I already defined earlier for use byscrollViewDidScroll::And here’s how I find the nearest page boundary in a vertical direction:
I discovered in testing that setting
targetContentOffsetdid not reliably force the scroll view to come to rest on a page boundary. For example, in the iOS 5 simulator, I could drag right/scroll left from page 5, stopping halfway to page 4, and even though I was settingtargetContentOffsetto page 4’s boundary, the scroll view would just stop scrolling with the 4/5 boundary in the middle of the screen.To work around this bug, we have to implement two more
UIScrollViewDelegatemethods. This one is called when the touch ends:And this one is called when the scroll view stops decelerating:
The End
As I said at the beginning, you can download my test implementation from my github repository and try it out for yourself.
That’s all, folks!