Google Now like interface on iOS

Siddharth Gupta picture Siddharth Gupta · May 4, 2013 · Viewed 9.2k times · Source

So, I absolutely love Google Now's cards interface on Android.. And recently it has even come to iOS.

Is there any tutorial, or sample project out there which can help me create a cards interface for my iOS applicaion?

From my research, I have been able to somewhat replicate "stacked" cards using a custom UICollectionViewFlowLayout.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *allAttributesInRect = [super layoutAttributesForElementsInRect:rect];

    CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMidY(self.collectionView.bounds));

    for (UICollectionViewLayoutAttributes *cellAttributes in allAttributesInRect)
    {
        if (CGRectContainsPoint(cellAttributes.frame, centerPoint))
        {
            cellAttributes.transform = CGAffineTransformIdentity;
            cellAttributes.zIndex = 1.0;
        }
        else
        {
            cellAttributes.transform = CGAffineTransformMakeScale(0.75, 0.75);
        }
    }

    return allAttributesInRect;
}

Then I set the minimum line spacing to a negative value to make them appear "stacked".

Upon scrolling though, I'd like the cards to stay at the bottom and only 1 car to scale up and center in the screen. Then I would scroll that card off screen and the next 'card' from the stack would scroll up from the stack and center on screen. I'm guessing this would be dynamically adjusting the minimum line spacing?

Answer

s1m0n picture s1m0n · May 12, 2013

I don't think there's any tutorial or class that does exactly what you want to do. However, if you don't mind only targeting iOS6 and above you can use an UICollectionView. Using a standard vertical flow layout it shouldn't be that hard to do what you're trying to achieve. Take a look at:

I know these examples don't look exactly like what you're trying to achieve. But once you grasp the basic concepts of UICollectionView using these sites, you'll be able to build the cards-layout in no time.

Update

I created a quick example to show a potential way to handle the panning 'away' of a cell. Make sure to add the necessary code to delete the item from the collection view at //Insert code to delete the cell here, it will then fill in the gap you created by removing the cell.

CLCollectionViewCell.h

#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>

@interface CLCollectionViewCell : UICollectionViewCell <UIGestureRecognizerDelegate>

@property (assign, setter = setDeleted:) BOOL isDeleted;
@property (strong) UIPanGestureRecognizer *panGestureRecognizer;

@end

CLCollectionViewCell.m

#import "CLCollectionViewCell.h"

@implementation CLCollectionViewCell

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super initWithCoder:aDecoder]) {
        // Create a pan gesture recognizer with self set as the delegate and add it the cell
        _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognizerDidChange:)];
        [_panGestureRecognizer setDelegate:self]; 
        [self addGestureRecognizer:_panGestureRecognizer];
        
        // Don't clip to bounds since we want the content view to be visible outside the bounds of the cell
        [self setClipsToBounds:NO];
        
        // For debugging purposes only: set the color of the content view
        [[self contentView] setBackgroundColor:[UIColor greenColor]];
    }
    return self;
}

- (void)panGestureRecognizerDidChange:(UIPanGestureRecognizer *)panGestureRecognizer {
    if ([self isDeleted]) {
        // The cell should be deleted, leave the state of the cell as it is 
        return;
    }
    
    // Percent holds a float value between -1 and 1 that indicates how much the user moved his finger relative to the width of the cell
    CGFloat percent = [panGestureRecognizer translationInView:self].x / [self frame].size.width;
    
    switch ([panGestureRecognizer state]) {
        case UIGestureRecognizerStateChanged: {
            // Create the 'throw animation' and base its current state on the percent
            CGAffineTransform moveTransform = CGAffineTransformMakeTranslation(percent * [self frame].size.width, 0.f);
            CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(percent * M_PI / 20.f);
            CGAffineTransform transform = CGAffineTransformConcat(moveTransform, rotateTransform) ;
            
            // Apply the transformation to the content view
            [[self contentView] setTransform:transform];
            
            break;
        }
            
        case UIGestureRecognizerStateFailed:
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            // Delete the current cell if the absolute value of the percent is above O.7 or the absolute value of the velocity of the gesture is above 600
            if (fabsf(percent) > 0.7f || fabsf([panGestureRecognizer velocityInView:self].x) > 600.f) {
                // The direction is -1 if the gesture is going left and 1 if it's going right
                CGFloat direction = percent < 0.f ? -1.f : 1.f;
                // Multiply the direction to make sure the content view will be removed entirely from the screen
                direction *= 1.5f;
                
                // Create the transform based on the direction of the gesture
                CGAffineTransform moveTransform = CGAffineTransformMakeTranslation(direction * [self frame].size.width , 0.f);
                CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(direction * M_PI / 20.f);
                CGAffineTransform transform = CGAffineTransformConcat(moveTransform, rotateTransform);
                
                // Calculate the duration of the animation based on the velocity of the pan gesture recognizer and normalize abnormal high and low values
                CGFloat duration = fabsf(1000.f / [panGestureRecognizer velocityInView:self].x);
                duration = duration > 2.f  ? duration = 2.f  : duration;
                duration = duration < 0.2f ? duration = 0.2f : duration;
                
                // Animate the 'throwing away' of the cell and update the collection view once it's completed
                [UIView animateWithDuration:duration
                                 animations:^(){
                                     [[self contentView] setTransform:transform];
                                }
                                 completion:^(BOOL finished){
                                     [self setDeleted:YES];
                                     
                                     // Insert code to delete the cell here
                                     // e.g. [collectionView deleteItemsAtIndexPaths:@[[collectionView indexPathForCell:self]]];
                }];
                
            } else {
                // The cell shouldn't be deleted: animate the content view back to its original position
                [UIView animateWithDuration:1.f animations:^(){
                    [[self contentView] setTransform:CGAffineTransformIdentity];
                }];
            }
            
            break;
        }
            
        default: {
            break;
        }
    }
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    // Return YES to make sure the pan gesture recognizer doesn't interfere with the gesture recognizer of the collection view
    return YES;
}

@end