Adding Interactivity to UICollectionView - iOS UICollectionView: The Complete Guide, Second Edition (2014)

iOS UICollectionView: The Complete Guide, Second Edition (2014)

Chapter 6. Adding Interactivity to UICollectionView

So far in this book, we’ve concentrated on every aspect of collection views except interactivity. With the exception of some basic copy/paste support provided to you by UICollectionViewDelegate, we have not focused on how the user interacts with the collection view beyond basic content offset changes. Sure, we’ve changed layouts in response to user interaction, but that’s not what I’m talking about. I’m asking this: How can we add interactivity directly to the collection view? How can we make truly immersive interfaces? The answer is gesture recognizers, which are the main focus of this chapter.

We’re going to take a look back at four code examples and augment them with new user interactivity. This is the polish that makes truly exceptional applications. After reading this chapter, you’ll have the skills to apply these same techniques to your own collection views.

Basic Gesture Recognizer

Let’s look back at our Better Survey example; the code for this chapter can be found under the Improved Better Survey project. We’ll add code to display a custom menu controller above a cell when it’s been long-pressed, as shown in Figure 6.1.

Image

Figure 6.1 Custom menu controller with long-press gesture

Remove the old UICollectionViewDelegate methods for cut/copy/paste support; we don’t need it anymore. Add the code shown in Listing 6.1 to your viewDidLoad implementation.

Listing 6.1 Setting Up the Long-Press Gesture Recognizer


gesutureRecognizer =
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)];
[self.collectionView addGestureRecognizer:longPressGestureRecognizer];


This creates a long-press gesture recognizer and attaches it to our collection view. It’s important to set up your gesture recognizers after your view has been set up. We could do this in loadView, but I prefer viewDidLoad because this is the sort of behavior we want only after the view has been loaded, and that’s exactly what viewDidLoad is for.

Note also that we’ve added the gesture recognizer to the collection view itself instead of each individual cell. This is less code and fewer objects in memory. As you’ll see shortly, UICollectionView offers an easy way to determine which cell (if any) is located at a certain point.

Now that we have our gesture recognizer set up, we need to respond to it. Add the method shown in Listing 6.2 to your view controller implementation.

Listing 6.2 Long-Press Gesture Recognizer Method


-(void)handleLongPress:(UILongPressGestureRecognizer *)recognizer
{
if (recognizer.state != UIGestureRecognizerStateBegan) return;

// Grab the location of the gesture and use it to locate the
// cell it was made on.
CGPoint point = [recognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView
indexPathForItemAtPoint:point];

// Check to make sure the long press was performed on a cell.
if (!indexPath)
{
return;
}

// Update our ivar for the menuAction: method
lastLongPressedIndexPath = indexPath;

// Grab our cell to display the menu controller from
UICollectionViewCell *cell = [self.collectionView
cellForItemAtIndexPath:indexPath];

// Create a custom menu item to hold the name of the model the
// cell is presenting
UIMenuItem *menuItem = [[UIMenuItem alloc]
initWithTitle:[[self photoModelForIndexPath:indexPath] name]
action:@selector(menuAction:)];

// Configure the shared menu controller and display it
UIMenuController *menuController = [UIMenuController
sharedMenuController];
menuController.menuItems = @[menuItem];
[menuController setTargetRect:cell.bounds inView:cell];
[menuController setMenuVisible:YES animated:NO];
}


This method is invoked whenever there is a change to the gesture recognizer. You want to note a couple of important things about this method. First, it checks for the state of the recognizer. Even though this is a long-press gesture recognizer and it doesn’t have a “changed” state, we need to make sure that it’s in its “began” state (instead of, for example, “possible”). In general, this is good practice when working with gesture recognizers.

Next, it finds the point under which the user is tapping. We have to check the index path returned; it will be nil if there is no cell under the tap.

We save our index path in an instance variable for use later; we need to remember which index path was most recently long-pressed. Finally, we grab the shared UIMenuController singleton and display a custom UIMenuItem from the cell itself. This will display the cut/copy/paste menu on the cell. The menuAction: selector is necessary; it cannot be nil, but it needs to be implemented in our class somewhere. We implement menuAction: in Listing 6.3.

Listing 6.3 Menu Controller Selector


-(void)menuAction:(id)sender
{
// Grab the last long-pressed index path, use it to find its
// corresponding model, and copy that to the pasteboard

UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
[pasteboard setString:[[self photoModelForIndexPath:lastLongPressedIndexPath]
name]];
}


This method is necessary because it will be invoked when the user taps on the custom menu item.

If you were to run the app right now, it would appear broken because the menu controller never appears. Why is that? The answer is a little obscure. UIViewController, which our view controller is a descendent of, extends UIResponder. The methods in this class are used byUIMenuController to determine whether a given object can display a menu controller. Specifically, we need to override canBecomeFirstResponder, shown in Listing 6.4, because the default is NO, and canPerformAction:withSender:, because we’ve created a custom action.

Listing 6.4 UIResponder Overridden Methods


- (BOOL)canPerformAction:(SEL)selector withSender:(id)sender
{
// Make sure the menu controller lets the responder chain know
// that it can handle its own custom menu action
if (selector == @selector(menuAction:))
{
return YES;
}

return [super canPerformAction:selector withSender:sender];
}

-(BOOL)canBecomeFirstResponder
{
// Must override to let the menu controller know it can be handled

return YES;
}


Now the long-press gesture recognizer will work as expected.

If you’re rolling your own gesture recognizers, you could run into trouble with interference from the built-in recognizers of UICollectionView. Look in UIScrollView.h for the tap and panGestureRecognizer and pinchGestureRecognizer properties. If you are having trouble implementing your own gesture recognizers, set up a UIGestureRecognizerDelegate and set up a chain of failing recognizers using require-GestureRecognizerToFail:.

Responding to Taps

As you saw, a Cover Flow layout isn’t that difficult. However, our current implementation lacks a little panache. It would be nice if, when tapping a noncentered cell, the collection view would center on that cell. It sounds easy, but as I found out, some strange behaviors aboutUICollectionView make it a little tricky.

The sample code for the new Cover Flow layout is called Improved Cover Flow. Let’s start by setting up another basic gesture recognizer in viewDidLoad, as shown in Listing 6.5.

Listing 6.5 Basic Tap Gesture Recognizer Setup


UITapGestureRecognizer *tapGestureRecognizer =
[[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleTapGestureRecognizer:)];
[self.collectionView addGestureRecognizer:tapGestureRecognizer];


Great. Before we implement handleTapGestureRecognizer:, we need to have some way to determine whether a cell is centered yet. Let’s go to the AFCoverFlowLayout class and add the following public method to the header and implementation file, as shown in Listing 6.6.

Listing 6.6 Determining Whether a Cell Is Centered


-(BOOL)indexPathIsCentered:(NSIndexPath *)indexPath
{
CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x,
self.collectionView.contentOffset.y,
CGRectGetWidth(self.collectionView.bounds),
CGRectGetHeight(self.collectionView.bounds));

UICollectionViewLayoutAttributes *attributes = [self
layoutAttributesForItemAtIndexPath:indexPath];

CGFloat distanceFromVisibleRectToItem =
CGRectGetMidX(visibleRect) - attributes.center.x;

return fabs(distanceFromVisibleRectToItem) < 1;
}


To determine whether a cell is centered, we rely on our existing layout’s methods to determine the attributes of an item at a given index path and check if that cell’s distance from the center of the currently visible frame is less than a small value (say, 1).

Next, we implement our gesture recognizer target method (see Listing 6.7).

Listing 6.7 Tap-to-Center Gesture Recognizer


-(void)handleTapGestureRecognizer:(UITapGestureRecognizer *)recognizer
{
if (self.collectionView.collectionViewLayout !=
coverFlowCollectionViewLayout) return;
if (recognizer.state != UIGestureRecognizerStateRecognized) return;

CGPoint point = [recognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView
indexPathForItemAtPoint:point];

if (!indexPath)
{
return;
}

BOOL centered = [coverFlowCollectionViewLayout
indexPathIsCentered:indexPath];

if (centered)
{
UICollectionViewCell *cell = [self.collectionView
cellForItemAtIndexPath:indexPath];

[UIView transitionWithView:cell duration:0.5f
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^{
cell.bounds = cell.bounds;
} completion:nil];
}
else
{
CGPoint proposedOffset = CGPointZero;
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
{
proposedOffset.x = indexPath.item *
(coverFlowCollectionViewLayout.itemSize.width +
coverFlowCollectionViewLayout.minimumLineSpacing);
}
else
{
proposedOffset.x = indexPath.item - 1) *
(coverFlowCollectionViewLayout.itemSize.width +
coverFlowCollectionViewLayout.minimumLineSpacing);
}

CGPoint contentOffset = [coverFlowCollectionViewLayout
targetContentOffsetForProposedContentOffset:proposedOffset
withScrollingVelocity:CGPointMake(0, 0)];

[self.collectionView setContentOffset:contentOffset animated:YES];
}
}


The first thing we do is check to make sure that the user tapped while in our Cover Flow layout. Then we check to make sure the gesture recognizer is in the correct state; this is common when dealing with gesture recognizers. We grab the point under the tap and its correspondingindexPath and check if it’s nil, just like last time.

Next, we determine whether the cell is already centered. If it is, we perform a cool flip animation similar to the iTunes app’s Cover Flow. If it is not centered yet, we make it centered with an animation by adjusting the content offset.

We can’t rely on UICollectionView’s scrollToItemAtIndexPath:scrollToItemAtIndexPath:animated: method because it will use the item’s location while uncentered to perform the calculation of where to go. The effect is that you’ll never center the item. Unfortunately, the answer is to calculate where the content offset would be if the item were already centered and scroll to there; we do this when we set proposedOffset.x.

This is just an estimate of where the cell would be if it were centered; we only have to get close enough for the Cover Flow layout to take over and readjust it. That’s why we call targetContentOffsetForProposedContentOffset:withScrollingVelocity:. It will go ahead and adjust our content offset to the exact offset we need to display our cell in the center of the screen.

Pinch and Pan Support

Let’s move on to a more recent example we can improve: the Circle Layout. The code for the Circle Layout with gesture support is labeled Improved Circle Layout.

We’re going to add a pinching gesture recognizer to the circle that will let the user reposition and resize the circle upon which our items fall. Luckily, our layout already exposes the center and radius properties, so adjusting them from the view controller is easy.

Currently, we’re setting the center and radius of the circle in prepareLayout. We’ll need to remove these because this method is called every time the layout is invalidated. Instead, we’ll put these in the viewDidLoad method of the view controller. We’ll also set up our pinch recognizer here, shown in Listing 6.8.

Listing 6.8 Updated viewDidLoad


// ... continued

// Set up circle layout

CGSize size = self.collectionView.bounds.size;
self.circleLayout.center = CGPointMake(size.width / 2.0,
size.height / 2.0);
self.circleLayout.radius = MIN(size.width, size.height) / 2.5;

// Set up gesture recognizers
UIPinchGestureRecognizer* pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
action:@selector(handlePinchGesture:)];
[self.collectionView addGestureRecognizer:pinchRecognizer];
}


We should also override the setters for the center and radius properties to automatically invalidate the layout for us, as shown in Listing 6.9.

Listing 6.9 Overridden Setters for Invalidating the Layout


-(void)setRadius:(CGFloat)radius
{
_radius = radius;

[self invalidateLayout];
}

-(void)setCenter:(CGPoint)center
{
_center = center;

[self invalidateLayout];
}


Great, we’re almost done. Really. The layout uses the center and radius properties to lay out the cells already, so we don’t have to change any more layout code. All we need to do is write the gesture recognizer method, which is shown in Listing 6.10.

Listing 6.10 Pinch Gesture Recognizer Method


- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer
{
static CGPoint initialLocation;
static CGPoint initialPinchLocation;
static CGFloat initialRadius;

if (self.collectionView.collectionViewLayout != self.circleLayout)
return;

if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialLocation = self.circleLayout.center;
initialPinchLocation = [recognizer
locationInView:self.collectionView];
initialRadius = self.circleLayout.radius;
}
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
CGPoint newLocation = [recognizer
locationInView:self.collectionView];

CGPoint translation;
translation.x = initialPinchLocation.x - newLocation.x;
translation.y = initialPinchLocation.y - newLocation.y;

CGFloat newScale = [recognizer scale];

self.circleLayout.center = CGPointMake(
initialLocation.x - translation.x,
initialLocation.y - translation.y);
self.circleLayout.radius = initialRadius * newScale;
}
}


When the user begins the gesture, we “remember” the original pinching location, circle center, and circle radius in some static variables. These will retain their values over each iteration of the method invocation. Then, whenever the gesture recognizer changes value, we recalculate the new values for center and radius, relying on the overridden setters to invalidate the layout for us.

Image

Figure 6.2 Variable circle layout

Let’s look at one more pinch layout. The stack layout that Mark Pospesel wrote already contains code for pinching open a stack (just like the Photos app on the iPad does with photo albums).

Listing 6.11 shows a bug with UICollectionView where it won’t remove supplementary views and decoration views when changing layouts; so for now, we need to do it ourselves.

Listing 6.11 Pinch Gesture Recognizer Method for Stacks


-(void)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
if (self.collectionView.collectionViewLayout != self.stackLayout)
return;

if (recognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint initialPinchPoint = [recognizer
locationInView:self.collectionView];
NSIndexPath* pinchedCellPath = [self.collectionView
indexPathForItemAtPoint:initialPinchPoint];
if (pinchedCellPath)
{
[self.stackLayout
setPinchedStackIndex:pinchedCellPath.section];
}
}
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
self.stackLayout.pinchedStackScale = recognizer.scale;
self.stackLayout.pinchedStackCenter = [recognizer
locationInView:self.collectionView];
}
else
{
if (self.stackLayout.pinchedStackIndex >= 0)
{
if (self.stackLayout.pinchedStackScale > 2.5)
{
[self.collectionView
setCollectionViewLayout:self.flowLayout animated:YES];
[self.navigationItem
setLeftBarButtonItem:[[UIBarButtonItem alloc]
initWithTitle:@"Back"
style:UIBarButtonItemStyleBordered
target:self action:@selector(goBack)] animated:YES];
}
else
{
// collapse items back into stack
NSMutableArray *leftoverViews = [NSMutableArray array];
for (UIView *subview in self.collectionView.subviews)
{
// Find all the supplementary views
if ([subview isKindOfClass:[AFCollectionViewHeaderView
class]])
{
[leftoverViews addObject:subview];
}
}
self.stackLayout.collapsing = YES;
[self.collectionView performBatchUpdates:^{
self.stackLayout.pinchedStackIndex = -1;
self.stackLayout.pinchedStackScale = 1.0;
} completion:^(BOOL finished) {
self.stackLayout.collapsing = NO;
// remove them from the view hierarchy
for (UIView *subview in leftoverViews)
[subview removeFromSuperview];
}];
}
}
}
}


Figure 6.3 shows the intermediate state of opening a stack with a pinching gesture.

Image

Figure 6.3 Pinching open a stack layout

Layout-to-Layout Transitions

iOS 7 introduced a new concept with UICollectionViewController: layout-to-layout transitions. These are handy, easy ways to interpolate between one collection view layout and another. The use case for this technique is when you need a noninteractive “push”-type transition (that is, tapping on a cell to see more detail). Note that the iOS 7 swipe-from-left-edge gesture will still work in its interactive manner.

To use layout-to-layout transitions, you’ll need to be using two UICollectionViewControllers within a navigation controller. One will push the other onto the navigation stack. Just before pushing that second view controller, set useLayoutToLayout-NavigationTransitions to YES. The second view controller’s collection view will use the same data source and delegate as the first. (As will be the case in our example, this is usually the first view controller itself.) The delegate outlet itself is set to the first view controller’s collection view’s delegate, but messages are forwarded to the second view controller itself. How Apple is doing this is unclear, and the documentation doesn’t specify much.

Our implementation is rather simple. We’re going to have a basic view controller with a basic flow layout set up in the app delegate, as shown in Listing 6.12.

Listing 6.12 Layout-to-Layout App Delegate


- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.

AFPrimaryLayout *layout = [[AFPrimaryLayout alloc] init];
AFPrimaryViewController *viewController = [[AFPrimaryViewController alloc]
initWithCollectionViewLayout:layout];

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
bounds]];
self.window.rootViewController = [[UINavigationController alloc]
initWithRootViewController:viewController];

[self.window makeKeyAndVisible];

return YES;
}


Next is the layout object. Its implementation is simple, as shown in Listing 6.13.

Listing 6.13 Layout-to-Layout Primary Layout


@implementation AFPrimaryLayout

-(id)init {
self = [super init];
if (self == nil) return nil;

self.itemSize = CGSizeMake(140, 140);

return self;
}

@end


Now that you’ve defined the app delegate and the layout, it’s time for our primary view controller itself. This is shown in Listing 6.14.

Listing 6.14 Layout-to-Layout Primary View Controller


@implementation AFPrimaryViewController

static NSString *CellIdentifier = @"Cell";

-(void)viewDidLoad {
[super viewDidLoad];

[self.collectionView registerClass:[UICollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];
}

-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section {
return 100;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView
dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

cell.backgroundColor = [UIColor purpleColor];

return cell;
}

-(void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[collectionView deselectItemAtIndexPath:indexPath animated:YES];

AFSecondaryViewController *viewController = [[AFSecondaryViewController
alloc] initWithCollectionViewLayout:[[AFSecondaryLayout alloc] init]];
viewController.useLayoutToLayoutNavigationTransitions = YES;
[self.navigationController pushViewController:viewController animated:YES];
}

@end


Note the bolded code. The secondary view controller’s implementation is completely empty. That’s because its collection view is relying on the primary view controller as the data source.

UIKit Dynamics

UIKit Dynamics are a new iOS 7 technology that uses a two-dimensional physics simulation to drive animations. They can also be used to drive collection view layouts, as discussed here.

The general use of UIKit Dynamics is beyond the scope of this book, but you can read more about them here. The crux of it is that a UIDynamicAnimator object drives the physics simulation, updating the center, size, and two-dimensional transform of a UIView or aUICollectionViewLayoutAttribute. We’re going to take a look at an open source example I’ve written. The source code is available on GitHub here.

This example uses UIKit Dynamics to reproduce the bouncy spring effect present in iOS 7’s Messages app.

When we initialize our dynamic animator, we pass it our collection view layout. This is important because the dynamic animator is going to be responsible for invalidating our layout whenever the underlying physics simulation changes. We’re going to subclass a flow layout so that we can rely on some logic in the superclass. We’re going to rely on this logic to update some spring-like behaviors in our animator. Each behavior is going to represent a layout element of our collection view.

So basically, we have a collection view layout that is going to own a dynamic animator. (Someone needs to own a strong reference to it.) That animator is going to contain spring-like attachment behaviors representative of the layout elements of our collection view. When we scroll, we’re going to rely on the logic in UICollectionViewFlow-Layout to update our behaviors.

Start with a basic application with a collection view controller, as shown in Listing 6.15.

Listing 6.15 Basic Collection View Controller


@implementation ASHCollectionViewController

static NSString * CellIdentifier = @"CellIdentifier";

-(void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];
}

-(UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}

-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.collectionViewLayout invalidateLayout];
}

#pragma mark - UICollectionView Methods

-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return 120;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView
dequeueReusableCellWithReuseIdentifier:CellIdentifier
forIndexPath:indexPath];

cell.backgroundColor = [UIColor orangeColor];
return cell;
}

@end


We’re invalidating the layout as soon as our view appears because the application this demo is in uses storyboards; this isn’t necessary if you set your collection views up using code. Let’s look at Listing 6.16, the private interface for our collection view layout.

Listing 6.16 Collection View Layout Interface


@interface ASHSpringyCollectionViewFlowLayout ()

@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;

@end


Nothing fancy here; just keeping a reference to the dynamic animator. Let’s also set up our basic properties in the initializer, as shown in Listing 6.17.

Listing 6.17 Collection View Layout Initializer


- (id)init
{
if (!(self = [super init])) return nil;

self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(44, 44);
self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];

return self;
}


Let’s next implement our prepare layout method. We can call the superclass’s implementation to lay out our collection view layout attributes according to the properties we set in the initializer. After we’ve prepared the layout in our superclass, we can use its implementation for determining the layout attributes in a given rect. Let’s look at the attributes in the rect defined by our entire content size, shown in Listing 6.18.

Listing 6.18 prepareLayout Implementation


[super prepareLayout];

CGSize contentSize = self.collectionView.contentSize;
NSArray *items = [super layoutAttributesForElementsInRect:
CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height)];


Note that this is incredibly inefficient. (Imagine if our collection view was even a little bigger; the number of items would take up a lot of memory simultaneously.) Iterating over all of them, as we’re about to do, would take up a lot of CPU time, as well.

We’ll need to check whether our dynamic animator already has behaviors for our items. If it does, and we add duplicate behaviors, we’ll get a runtime exception. Listing 6.19 shows our implementation.

Listing 6.19 Collection View Layout Interface


if (self.dynamicAnimator.behaviors.count == 0) {
[items enumerateObjectsUsingBlock:^(id<UIDynamicItem> obj, NSUInteger idx,
BOOL *stop) {
UIAttachmentBehavior *behaviour = [[UIAttachmentBehavior alloc]
initWithItem:obj

attachedToAnchor:[obj center]];

behaviour.length = 0.0f;
behaviour.damping = 0.8f;
behaviour.frequency = 1.0f;

[self.dynamicAnimator addBehavior:behaviour];
}];
}


For each item in the full content rect of the collection view, we create an attachment behavior based on that item, configure it, and add it to the dynamic animator. I chose those values for the properties on the behaviors because they seemed nice, experimentally.

Next, we need to forward inquiries about the state of our collection view layout attributes to our dynamic animator (see Listing 6.20). This is relatively straightforward, because dynamic animators were designed specifically to work with collection views.

Listing 6.20 Forwarding Messages to the Dynamic Animator


-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return [self.dynamicAnimator itemsInRect:rect];
}

-(UICollectionViewLayoutAttributes
*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}


The next step is to respond to scrolling events. We’re going to do this in a slightly roundabout way: We’re going to override the super implementation of should-InvalidateLayoutForBoundsChange. This method is called whenever the bounds of the collection view changes, such as when it’s scrolled by the user’s finger (shown in Listing 6.21).

Listing 6.21 Responding to Scrolling


-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
UIScrollView *scrollView = self.collectionView;
CGFloat delta = newBounds.origin.y - scrollView.bounds.origin.y;

CGPoint touchLocation = [self.collectionView.panGestureRecognizer
locationInView:self.collectionView];

[self.dynamicAnimator.behaviors
enumerateObjectsUsingBlock:^(UIAttachmentBehavior *springBehaviour, NSUInteger
idx, BOOL *stop) {
CGFloat yDistanceFromTouch = fabsf(touchLocation.y -
springBehaviour.anchorPoint.y);
CGFloat xDistanceFromTouch = fabsf(touchLocation.x -
springBehaviour.anchorPoint.x);
CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) /
1500.0f;

UICollectionViewLayoutAttributes *item =
springBehaviour.items.firstObject;
CGPoint center = item.center;
if (delta < 0) {
center.y += MAX(delta, delta*scrollResistance);
}
else {
center.y += MIN(delta, delta*scrollResistance);
}
item.center = center;

[self.dynamicAnimator updateItemUsingCurrentState:item];
}];

return NO;
}


There’s a lot of math in there; don’t worry, though, we’ll tease it apart. First, we calculate the change in content offset y (that is, how much the user has scrolled by since the last time this method was called). Next, we determine where the user is touching on the collection view. This is important because we want items closer to the user’s finger to scroll more rapidly and want items farther away to lag behind a bit more.

For each behavior in our dynamic animator, we divide the sum of the x and y deltas by a denominator of 1500, a value determined experimentally. Use a smaller denominator to make the collection view react with more “spring.” This is like a “resistance” to the scrolling of the collection view. We then, finally, cap that product at a min or max of the delta. This prevents the delta from being negative and having items really far away from the user’s finger scrolling in the opposite direction than they’re supposed to.

Finally, notice that we return NO to the method. Because the dynamic animator is going to take care of invalidating our layout, we don’t have to do so here.

That’s really all there is to it. You can build and run the application, or you can download an animated GIF of the collection view in action here. This rather naïve approach works for collection views with up to a few hundred items. This is sufficient for the scope of this book. If you’d like to learn more about how to tile the behaviors, take a look at a tutorial I wrote on the topic at obj.io.

Now that you understand how gesture recognizers can be used to interact with collection views—via their layouts—and understand the basics of using UIKit Dynamics to back a collection view layout, you’re ready to create truly immersive, awesome applications. Good luck!