Animate a UICollectionView cell on selection - ios

I have a basic grid in a UICollectionView. It's a simple 2 column, multiple row layout using the UICollectionViewDelegateFlowLayout. When a cell is selected, I want to dim the background, float the cell to the center of the screen and then have a workflow based on the selected cell. I'm fairly new to UICollectionViews, and I'm not sure of the best way to go about this.
Should I have a custom layout of the UICollectionView for when a cell is selected?
Or is there a way I can animate the selected cell without having to create a new layout
If anyone can just get me going on the right direction, I think I'll be good to research how to execute it.

After trying several (and rather hacky) solutions, I solved this by writing my own UICollectionView layout. Previous solutions I attempted:
Add a custom UIView overtop the UICollectionView and attempt to fake the original cell moving by overlaying it, dimming the background, and animating the new UIView
Animate the cell without creating a brand new layout
I was trying to avoid it, but after I got into coding it, it was a lot less painful than I originally thought.
Here is my code, for anyone interested:
.h:
#interface FMStackedLayout : UICollectionViewLayout
#property(nonatomic,assign) CGPoint center;
#property(nonatomic,assign) NSInteger cellCount;
-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath;
#end
.m:
#define ITEM_WIDTH 128.0f
#define ITEM_HEIGHT 180.0f
static NSUInteger const RotationCount = 32;
static NSUInteger const RotationStride = 3;
static NSUInteger const PhotoCellBaseZIndex = 100;
#interface FMStackedLayout ()
#property(strong,nonatomic) NSArray *rotations;
#property(strong,nonatomic) NSIndexPath *selectedIndexPath;
#end
#implementation FMStackedLayout
#pragma mark - Lifecycle
-(id)initWithSelectedCellIndexPath:(NSIndexPath *)indexPath{
self = [super init];
if (self) {
self.selectedIndexPath = indexPath;
[self setup];
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self){
[self setup];
}
return self;
}
-(void)setup{
NSMutableArray *rotations = [NSMutableArray arrayWithCapacity:RotationCount];
CGFloat percentage = 0.0f;
for (NSUInteger i = 0; i < RotationCount; i++) {
// Ensure that each angle is different enough to be seen
CGFloat newPercentage = 0.0f;
do {
newPercentage = ((CGFloat)(arc4random() % 220) - 110) * 0.0001f;
} while (fabsf(percentage - newPercentage) < 0.006);
percentage = newPercentage;
CGFloat angle = 2 * M_PI * (1.0f + percentage);
CATransform3D transform = CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
[rotations addObject:[NSValue valueWithCATransform3D:transform]];
}
self.rotations = rotations;
}
#pragma mark - Layout
-(void)prepareLayout{
[super prepareLayout];
CGSize size = self.collectionView.frame.size;
self.cellCount = [self.collectionView numberOfItemsInSection:0];
self.center = CGPointMake(size.width / 2.0, size.height / 2.0);
}
-(CGSize)collectionViewContentSize{
return self.collectionView.frame.size;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.size = CGSizeMake(ITEM_WIDTH, ITEM_HEIGHT);
attributes.center = self.center;
if (indexPath.item == self.selectedIndexPath.item) {
attributes.zIndex = 100;
}else{
attributes.transform3D = [self transformForPersonViewAtIndex:indexPath];
}
return attributes;
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger i = 0; i < self.cellCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i
inSection:0];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
#pragma mark - Private
-(CATransform3D)transformForPersonViewAtIndex:(NSIndexPath *)indexPath{
NSInteger offset = (indexPath.section * RotationStride + indexPath.item);
return [self.rotations[offset % RotationCount] CATransform3DValue];
}
#end
Then, on your UICollectionView, you call
MyLayout *stackedLayout = [[MyLayout alloc] initWithSelectedCellIndexPath:indexPath];
[stackedLayout invalidateLayout];
[self.collectionView setCollectionViewLayout:stackedLayout
animated:YES];
Animations between cells will be handled by the custom layout.

Seems to be a multi-part question, so I'll answer part of it.
UICollectionviewCells do not automatically adjust on highlight or selection. It will only tell you when the cell is highlighted or selected, with one exception:
For the most part, the collection view modifies only the properties of
a cell to indicate that it is selected or highlighted; it does not
change the visual appearance of your cells, with one exception. If a
cell’s selectedBackgroundView property contains a valid view, the
collection view shows that view when the cell is highlighted or
selected.
Otherwise, you must do the visual highlighting manually. Usually by adjusting either the .alpha property of the entire cell, or swapping out the .image property of a background UIImageView in the cell itself using one or more of the following methods, which are accessible in the <UICollectionViewDelegate> Protocol:
collectionView:didDeselectItemAtIndexPath:
collectionView:didHighlightItemAtIndexPath:
collectionView:didSelectItemAtIndexPath:
collectionView:didUnhighlightItemAtIndexPath:
collectionView:shouldDeselectItemAtIndexPath:
collectionView:shouldHighlightItemAtIndexPath:
collectionView:shouldSelectItemAtIndexPath:
As for the rest of your question,I wish I could be of more help, but I'm not as fluent at the custom view animation.

Related

iOS: UICollectionView repeat NSMutableArray value in circular way

How can I repeat NSMutableArray value in UICollectionView and scroll automatically from top to bottom for gaming like effect?
For example if I have array with 10 elements and wants to repeat all 10 value in UICollectionView with 4 cells in a row and scroll from top to bottom continuously.
Currently I am adding 200 values statically in NSMutableArray and than applying NSTimer on that. But I want to repeat 10 values again and again.
How can I do that?
code given below is to call on NSTimer for scrolling
[collectionview setUserInteractionEnabled:NO];
CGFloat h = collectionview.contentOffset.y;
h += 10;
CGFloat h2 = 10;
h2 += 10;
CGPoint bottomOffset = CGPointMake(0, h - h2);
[collectionview setContentOffset:bottomOffset animated:YES];
int dd = collectionview.contentOffset.y;
NSLog(#"dd timer....%d" ,dd);
if (dd <= 0) {
}
I have a similar situation where I continuously scroll a collection of images. What I did was in collectionView:numberOfItemsInSection: was return INT_MAX. Then in collectionView:cellForItemAtIndexPath:, just mod the row by the size of your NSMutableArray and use that as the index into your array to create the UICollectionViewCell. It doesn't scroll backwards from the top of the list, and it isn't exactly "infinite", but it sure does provied a LOT of cells. I suppose if you wanted to start in the middle and allow scrolling backwards, you could just scroll the collection view to some cell that makes sense in the middle of the collection.
Example implementation:
#interface CollectionViewController ()
#property (nonatomic, strong) NSArray<NSString*>* objects;
#end
#implementation CollectionViewController
static NSString * const reuseIdentifier = #"Cell";
- (void)viewDidLoad
{
[super viewDidLoad];
_objects = #[#"One", #"Two", #"Three", #"Four", #"Five", #"Six", #"Seven", #"Eight", #"Nine", #"Ten"];
}
#pragma mark <UICollectionViewDataSource>
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return INT_MAX;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell* cell = (CollectionViewCell*)[collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
NSInteger index = indexPath.row % self.objects.count;
NSString* str = self.objects[index];
[cell.label setText:str];
return cell;
}
#end
I don't know for sure what the performance impact of this is, but since UICollectionView only loads as many cells as it needs, I don't believe it to be too impactful. It works well for me.
If true circular scrolling is important, you could try an open-source solution (ex. https://cocoapods.org/pods/PMCircularCollectionView)
You can try this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[yourMutableArray addObjectsFromArray: yourMutableArray];
[yourCollectionView reloadData];
}
}

Get the frame for a UICollectionView section

How to get the frame (CGRect value) of a whole section in a UICollectionView instance?
I know that UITableView has a rectForSection: method. I am looking for a similar thing for UICollectionView.
I checked the documents for UICollectionViewFlowLayout but couldn't find anything that looks like rectForSection:.
to collectionView add category
#implementation UICollectionView (BMRect)
- (CGRect)bm_rectForSection:(NSInteger)section {
NSInteger sectionNum = [self.dataSource collectionView:self numberOfItemsInSection:section];
if (sectionNum <= 0) {
return CGRectZero;
} else {
CGRect firstRect = [self bm_rectForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
CGRect lastRect = [self bm_rectForRowAtIndexPath:[NSIndexPath indexPathForItem:sectionNum-1 inSection:section]];
return CGRectMake(0, CGRectGetMinY(firstRect), CGRectGetWidth(self.frame), CGRectGetMaxY(lastRect) - CGRectGetMidY(firstRect));
}
}
- (CGRect)bm_rectForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
#end
Collection view section frame is depend on the collection view layout.It might be flow layout or custom layout. So you will not got the any predefined solution. But you can calculate it for your layout using
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
I will suggest you to subclass UICollectionViewLayout and put you section frame calculation code inside it, so that you section frame will always be the updated one.
NOTE : These point will not give you superset of rect in case of section start point and end point not vertically aligned.To be honest there is no better way to get section frame.

How do I create a UICollectionView with column and row headers?

I want to create a UICollectionView that looks like this:
It won't be scrollable or editable. I'm currently wondering how to write the layout for this. I'm guessing it won't be a subclass of UICollectionViewFlowLayout. I can think of a number of ways, but was curious if there was any "right" way. The cells will be animate-able.
Should each row or column be its own section?
I've done something like what you want with a subclass of UICollectionViewFlowLayout. It looks like this,
#implementation MultpleLineLayout {
NSInteger itemWidth;
NSInteger itemHeight;
}
-(id)init {
if (self = [super init]) {
itemWidth = 80;
itemHeight = 80;
}
return self;
}
-(CGSize)collectionViewContentSize {
NSInteger xSize = [self.collectionView numberOfItemsInSection:0] * (itemWidth + 2); // the 2 is for spacing between cells.
NSInteger ySize = [self.collectionView numberOfSections] * (itemHeight + 2);
return CGSizeMake(xSize, ySize);
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
NSInteger xValue;
attributes.size = CGSizeMake(itemWidth,itemHeight);
xValue = itemWidth/2 + path.row * (itemWidth +2);
NSInteger yValue = itemHeight + path.section * (itemHeight +2);
attributes.center = CGPointMake(xValue, yValue);
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSInteger minRow = (rect.origin.x > 0)? rect.origin.x/(itemWidth +2) : 0; // need to check because bounce gives negative values for x.
NSInteger maxRow = rect.size.width/(itemWidth +2) + minRow;
NSMutableArray* attributes = [NSMutableArray array];
for(NSInteger i=0 ; i < self.collectionView.numberOfSections; i++) {
for (NSInteger j=minRow ; j < maxRow; j++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
return attributes;
}
The data is arranged as an array of arrays where each inner array supplies the data for one horizontal row. With the values I have in there now, and using your data as an example, the view looked like this,
This is the code I haven the view controller,
#interface ViewController ()
#property (strong,nonatomic) UICollectionView *collectionView;
#property (strong,nonatomic) NSArray *theData;
#end
#implementation ViewController
- (void)viewDidLoad {
self.theData = #[#[#"",#"A",#"B",#"C"],#[#"1",#"115",#"127",#"132"],#[#"2",#"",#"",#"153"],#[#"3",#"",#"199",#""]];
MultpleLineLayout *layout = [[MultpleLineLayout alloc] init];
self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.view.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.collectionView];
[self.collectionView registerNib:[UINib nibWithNibName:#"CustomDataCell" bundle:nil] forCellWithReuseIdentifier:#"DataCell"];
}
- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
return [self.theData[section] count];
}
- (NSInteger)numberOfSectionsInCollectionView: (UICollectionView *)collectionView {
return [self.theData count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
DataCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"DataCell" forIndexPath:indexPath];
cell.label.text = self.theData[indexPath.section][indexPath.row];
return cell;
}
You can do it with UICollectionViewFlowLayout but if you go that way you need to count carefully and get the padding (i.e. the top left, and other "empty" cells) right. If you miscount it's obvious because the flow ruins everything quickly and you will find your header cells halfway down your columns. (I have done a similar thing)
I only did it that way because of a fear of custom layouts - but in fact it is as easy as UITableView. If your layout does not scroll at all then yours will be particularly simple as your only real work will be calculating frame values for cells, to be returned in layoutAttributesForItemAtIndexPath. Since your whole view fits in the visible area, layoutAttributesForElementsInRect will mean you simply iterate through all the cells in the view. collectionViewContentSize will be the size of your view.
Looking at your sample picture you might find it convenient to organise your data as a dictionary of arrays, one per column. You can get a column array by name ("A", "B" etc.) and the position in the array corresponds to the value in the leftmost column, which you might name "index".
There are many more methods you can use but those are the basics, and will get your basic display up and running.

iOS UICollectionView header & footer location

Working in iOS 7, how does one specify where the header & footer boxes go in a UICollectionView?
I have a custom UICollectionViewFlowLayout. I have overwritten
-(void)prepareLayout
-(NSArray*) layoutAttributesForElementsInRect:(CGRect)rect
-(UICollectionViewLayoutAttributes*) layoutAttributesForSupplementaryViewOfKind: (NSString*)kind atIndexPath:(NSIndexPath*)indexPath
My problem is, I'm not sure how to specify header location. I have already specified that a header exists in prepareLayout:
-(void)prepareLayout
{
[super prepareLayout];
boundsSize = self.collectionView.bounds.size;
midX = boundsSize.width / 2.0f;
curIndex = 0;
self.headerReferenceSize = CGSizeMake(CELL_SIZE, TITLE_HEIGHT);
self.footerReferenceSize = CGSizeMake(0, 0);
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(TOP_INSET, LEFT_INSET, BOTTOM_INSET, RIGHT_INSET);
self.minimumLineSpacing = LINE_SPACING;
self.minimumInteritemSpacing = INTERIM_SPACING;
self.itemSize = CGSizeMake(CELL_SIZE, CELL_SIZE);
}
I just don't know the right property of my custom FlowLayout to set, as there doesn't seem to be something like "HeaderLocation" to set, either as a LayoutAttributes or in the layout object itself. Right now, it is appearing to the side/between my images, when I'd like them to be appearing above each image (horizontal scroll).
I have tried the following:
-(UICollectionReusableView*) collectionView: (UICollectionView*)collectionView viewForSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath
{
NSLog(#"**ViewForSupplementaryElementOfKind called***");
CGFloat centerX = collectionView.center.x;
CGFloat centerY = collectionView.center.y;
CGFloat titleWidth = [MyLayout titleWidth];
CGFloat titleHeight = [MyLayout titleHeight];
MyTitleView* titleView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:ImageTitleIdentifier forIndexPath:indexPath];
titleView.frame = CGRectMake(centerX - titleWidth/2.0,
0.0,
titleWidth,
titleHeight);
return titleView;
}
This doesn't work. The title appears above overlapped with a bunch of other titles, then the moment I start scrolling (horizontally), they jump back into the wrong place, horizontally between the images rather than above.
PS> Please do not suggest anything that has to do with NIB or XIB placement. I am using a UICollectionView, NOT a UICollectionViewController, so I actually have no prototypical cell to work with. The layout is being done entirely programatically -- from code alone -- so I can't simply open a XIB file and adjust the location of a text box.
Amending the attributes returned by -layoutAttributesForElementsInRect is the right approach, but if you want to alter the position of offscreen headers and footers, you may need to fetch the supplementary view attributes yourself.
For example, in your UICollectionViewFlowLayout subclass:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributesArray = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
// the call to super only returns attributes for headers that are in the bounds,
// so locate attributes for out of bounds headers and include them in the array
NSMutableIndexSet *omittedSections = [NSMutableIndexSet indexSet];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if (attributes.representedElementCategory == UICollectionElementCategoryCell) {
[omittedSections addIndex:attributes.indexPath.section];
}
}
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
[omittedSections removeIndex:attributes.indexPath.section];
}
}
[omittedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:indexPath];
[attributesArray addObject:attributes];
}];
for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
if ([attributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
// adjust any aspect of each header's attributes here, including frame or zIndex
}
}
return attributesArray;
}
CollectionView Header height is set below Collectionview delegate
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
And Set view in Collectionview Header in Below Delegate
- (UICollectionReusableView*)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionReusableView * view = nil;
if ([kind isEqualToString:UICollectionElementKindSectionHeader])
{
ColorSectionHeaderView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:NSStringFromClass([ColorSectionHeaderView class])
forIndexPath:indexPath];
header.sectionIndex = indexPath.section;
header.hideDelete = collectionView.numberOfSections == 1; // hide when only one section
header.delegate = self;
view = header;
}
return view;
}
Ragistred Class in ViewDidLoad
-(void)ViewDidLoad
{
[collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([ColorSectionFooterView class]) bundle:nil]
forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
withReuseIdentifier:NSStringFromClass([ColorSectionFooterView class])];
[Super ViewDidLoad];
}

Subclassing UICollectionViewLayout and assign to UICollectionView

I have a CollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// assign layout (subclassed below)
self.collectionView.collectionViewLayout = [[CustomCollectionLayout alloc] init];
}
// data source is working, here's what matters:
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ThumbCell *cell = (ThumbCell *)[cv dequeueReusableCellWithReuseIdentifier:#"ThumbCell" forIndexPath:indexPath];
return cell;
}
I also have a UICollectionViewLayout subclass: CustomCollectionLayout.m
#pragma mark - Overriden methods
- (CGSize)collectionViewContentSize
{
return CGSizeMake(320, 480);
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
return [[super layoutAttributesForElementsInRect:rect] mutableCopy];
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
// configure CellAttributes
cellAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
// random position
int xRand = arc4random() % 320;
int yRand = arc4random() % 460;
cellAttributes.frame = CGRectMake(xRand, yRand, 150, 170);
return cellAttributes;
}
I'm trying to use a new frame for the cell, just one at a random position.
The problem is layoutAttributesForItemAtIndexPath is not called.
Thanks in advance for any advice.
Edit: I didn't notice that you've solved your case, but I had the same problem and here's what helped in my case. I'm leaving it here, it may be useful for someone given that there's not much on this topic yet.
Upon drawing, UICollectionView does not call the method layoutAttributesForItemAtIndexPath: but layoutAttributesForElementsInRect: to determine which cells should be visible. It's your responsibility to implement this method and from there call layoutAttributesForItemAtIndexPath: for all visible index paths to determine exact locations.
In other words, add this to your layout:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray * array = [NSMutableArray arrayWithCapacity:16];
// Determine visible index paths
for(???) {
[array addObject:[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:??? inSection:???]]];
}
return [array copy];
}
Was an issue with Storyboard parameters. Assigned custom UICollectionLayout from Inspector panel.

Resources