UICollectionView using sections leaves a gap between cells - ios

I'm using a UICollectionView to show several sections of data. These sections have a fixed number of items. I want all the items to show in a continuous grid.
Right now I accomplish this in horizontal orientation:
But in vertical this leaves a big gap:
I want to solve this gap between sections because it's ugly and it doesn't belong there.
I'd be happy to use a custom FlowLayout, but I can't find a tutorial that points me in the right direction (I've found several, but none of them really touch this problem specifically.)
Can anybody help me solve this problem, or at least point me in the right direction?
P.S: I've implemented sections because I'm loading the data on the fly. Using 1 section isn't an option for me at this moment.
UPDATE
On request I'm adding the values used for my current FlowLayout. I'm using the standard Horizontal Flow Layout on a fullscreen (minus UINavigationBar) UICollectionView with 21 items per section.
Scroll direction: Horizontal
Cell size: 248, 196
Header / footer size: none
Min spacing for cells: 10
Min spacing for lines: 10
Section Insets: 20, 20, 10, 10

Use like this
It will solve Gap problem
UICollectionViewFlowLayout *layout=[[UICollectionViewFlowLayout alloc] init];
layout.minimumInteritemSpacing = 0;
layout.minimumLineSpacing = 2;

To remove those gaps you need to create custom layout which will act as layout for your collection view. This class will child class for UICollectionViewFlowLayout.
Then you can override below two methods and can create your own custom layout as you want.
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
UICollectionViewLayoutAttributes is class which will deal with cell position, frame, Zindex etc
You can also use below properties.
collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
collectionView:layout:minimumLineSpacingForSectionAtIndex:

Related

how to develop a custom UICollectionViewLayout that has staggered columns with self-sizing cells?

I'm working on the iOS version of an app I already developed on Android. This app has the following 2 column grid of self-sizing (fixed width but variable height) cells:
Achieving this in the Android version was easy because Google provides a StaggeredGridLayoutManager for its RecyclerView. You specify the number of columns and the direction of the scroll and you are done.
The default UICollectionView layout UICollectionViewFlowLayout doesn't allow the staggered layout I'm looking for, so I have to implement a custom layout. I have watched 2 WWDC videos that talk about this topic (What's New in Table and Collection Views and Advanced User Interfaces with Collection Views) and I more or less have an idea of how it should be implemented.
Step 1. First an approximation of the layout is computed.
Step 2. Then the cells are created and sized with autolayout.
Step 3. Then the controller notifies the of the cell sizes so the layout is updated.
My doubts come when trying to code these steps. I found a tutorial that explains the creation of a custom layout with staggered columns, but it doesn't use autolayout to obtain the size of the cells. Which leaves me with the following questions:
In step 2, how and when can I obtain the cell size?
In step 3, how and when can I notify the layout of the changes?
I want to point out that, as you have mentioned, RayWenderlich PinInterest Layout is exactly the tutorial that'll help you achieve this layout.
To answer your questions - with regards to the tutorial:
In step 2, how and when can I obtain the cell size?
To get the cell height, a delegate method was implemented that was called in the prepareLayout method of the custom UICollectionViewLayout. This method is called once (or twice, I just attempted to run it with a print statement, and I got two calls). The point of prepareLayout is to initialize the cell's frame property, in other words, provide the exact size of each cell. We know that the width is constant, and only the height is changing, so in this line of prepareLayout:
let cellHeight = delegate.collectionView(collectionView!,
heightForItemAtIndexPath: indexPath, withWidth: width)
We obtain the height of the cell from the delegate method that was implemented in the UICollectionViewController. This happens for all the cells we want to show in the collectionView. After obtaining and modifying the height for each cell, we cache the result so we can inspect it later.
Afterwards, for the collectionView to obtain the size of each cell on screen, all it needs to do is query the cache for the information. This is done in layoutAttributesForElementsInRect method of your custom UICollectionViewLayout class.
This method is called automatically by the UICollectionViewController. When the UICollectionViewController needs layout information for cells that are coming onto the screen (as a result of scrolling, for instance, or upon first load), you return the attributes from the cache that you've populated in prepareLayout.
In conclusion to your question: In step 2, how and when can I obtain the cell size?
Answer: Each cell size is obtained within the prepareLayout method of your custom UICollectionViewFlowLayout, and is calculated early in the life cycle of your UICollectionView.
In step 3, how and when can I notify the layout of the changes?
Note that the tutorial does not account for new cells to be added at runtime:
Note: As prepareLayout() is called whenever the collection view’s layout is invalidated, there are many situations in a typical implementation where you might need to recalculate attributes here. For example, the bounds of the UICollectionView might change – such as when the orientation changes – or items may be added or removed from the collection. These cases are out of scope for this tutorial, but it’s important to be aware of them in a non-trivial implementation.
Like he wrote, it's a non trivial implementation that you might need. There is, however, a trivial (very inefficient) implementation that you might adopt if your data set is small (or for testing purposes). When you need to invalidate the layout because of screen rotation or adding/removing cells, you can purge the cache in the custom UICollectionViewFlowLayout to force prepareLayout to reinitialize the layout attributes.
For instance, when you have to call reloadData on the collectionView, also make a call to your custom layout class, to delete the cache:
cache.removeAll()
I realise this is not a complete answer, but some pointers regarding your steps 2 and 3 may be found in the subclassing notes for UICollectionViewLayout.
I presume you have subclassed UICollectionViewFlowLayout since off the top of my head I believe this is a good starting point for making adjustments to the layout to get the staggered appearance you want.
For step 2 layoutAttributesForElementsInRect(_:) should provide the layout attributes for the self sized cells.
For step 3 your layout will have shouldInvalidateLayoutForPreferredLayoutAttributes(_:withOriginalAttributes:) called with the changed cell sizes.
In step 2, how and when can I obtain the cell size?
You need to calculate height of each cell in prepareLayout() method. Result of calculation for each cell should be assigned to UICollectionViewLayoutAttributes variable, and than put it into collection NSDictionary, where key would be NSIndexPath(of each cell), and value would be UICollectionViewLayoutAttributes variable.
Example:
- (void)prepareLayout {
[_layoutMap removeAllObjects];
_totalItemsInSection = [self.collectionView numberOfItemsInSection:0];
_columnsYoffset = [self initialDataForColumnsOffsetY];
if (_totalItemsInSection > 0 && self.totalColumns > 0) {
[self calculateItemsSize];
NSInteger itemIndex = 0;
CGFloat contentSizeHeight = 0;
while (itemIndex < _totalItemsInSection) {
NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:itemIndex inSection:0];
NSInteger columnIndex = [self columnIndexForItemAtIndexPath:targetIndexPath];
// you need to implement this method and perform your calculations
CGRect attributeRect = [self calculateItemFrameAtIndexPath:targetIndexPath];
UICollectionViewLayoutAttributes *targetLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:targetIndexPath];
targetLayoutAttributes.frame = attributeRect;
contentSizeHeight = MAX(CGRectGetMaxY(attributeRect), contentSizeHeight);
_columnsYoffset[columnIndex] = #(CGRectGetMaxY(attributeRect) + self.interItemsSpacing);
_layoutMap[targetIndexPath] = targetLayoutAttributes;
itemIndex += 1;
}
_contentSize = CGSizeMake(self.collectionView.bounds.size.width - self.contentInsets.left - self.contentInsets.right,
contentSizeHeight);
}
}
Don't forget to implement following methods:
- (NSArray <UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray<UICollectionViewLayoutAttributes *> *layoutAttributesArray = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *layoutAttributes in _layoutMap.allValues) {
if (CGRectIntersectsRect(layoutAttributes.frame, rect)) {
[layoutAttributesArray addObject:layoutAttributes];
}
}
return layoutAttributesArray;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return _layoutMap[indexPath];
}
These methods would be triggered once you call reloadData() mehtod or invalidateLayout().
In step 3, how and when can I notify the layout of the changes?
Just call self.collectionView.collectionViewLayout.invalidateLayout() and prepareLayout() method would be called once again, so you can recalculate all parameters you need.
You can find my full tutorial about custom UICollectionViewLayout here: https://octodev.net/custom-collectionviewlayout/
Tutorial contains implementation in both languages: Swift and Objective-C.
Would be more than glad to answer all your questions.
The "cell size" is defined by UICollectionViewLayoutAttribute in the layout subclass which mean you can modify it every time you have the chance to touch them. You can set every attributes' size to what you desire.
For example you can do it in layoutAttributesOfElementsInRect(:) , calculate the right size and config all attributes before pass them to collectionView. You can also do it in layoutAttributeOfItemAtIndexPath(:) ,make the calculation when every attribute is created.
Furthermore, consider to provide the desired size by a datasource so every attribute can easily get their size with their index.
For if you want to have the cell size to layout the subviews in a cell, do it in the collectionView delegate method: collectionView:ItemAtIndexPath:
Hope this help.

UICollectionViewFlowLayout minimumInteritemSpacing doesn't work

I've got two problems with my UICollectionView:
minimumInteritemSpacing doesn't work
it overflows horizontally on iOS 6
I set up the layout like this:
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(70.0f, 70.0f);
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.minimumLineSpacing = 0.0f;
layout.minimumInteritemSpacing = 0.0f;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
// I set the size of _collectionView in layoutSubviews:
// _collectionView.frame = self.bounds;
_collectionView.contentInset = UIEdgeInsetsMake(8.0f, 8.0f, 8.0f, 8.0f);
The image shows the result on iOS 6 (on iOS 7 there is no overflow, but the spacing between columns is still not zero)
I tried this solution https://gist.github.com/OliverLetterer/5583087, but it doesn't fix anything in my case.
From the documentation for the minimumInterItemSpacing property:
For a horizontally scrolling grid, this value represents the minimum spacing between items in the same column. This spacing is used to compute how many items can fit in a single line, but after the number of items is determined, the actual spacing may possibly be adjusted upward.
The flow layout will evenly space cells across its width, with a spacing of no smaller than the minimum you set. If you don't want the spacing, you'll need to implement your own layout.
The iOS 6 overflow issue I'm not sure about. Try dropping support for iOS 6 ;)
"line spacing" can mean the space between vertical lines.
Say you have a ordinary single line horizontal collection view.
(Eg, the whole view is simply 50 high, and the items are simply 50x50.)
As user #matt has explained
a horizontal collection view has columns of cells
the "lines" as Apple means it are the vertical "lines" (!) of cells
Thus:
in the case of a simple horizontal collection view with one row,
what Apple names the "line" spacing is - indeed - the spacing between items
Thus surprisingly in a simple horizontal collection view with one row, to set the gap between items, it's just:
l.minimumLineSpacing = 6 // Apple means "vertical scan lines" by "lines"
(minimumInteritemSpacing is completely meaningless in a normal simple horizontal collection view with one row.)
This finally explains why there are 100 pages on the internet asking why minimumInteritemSpacing just doesn't work. Fantastic tip by user #matt

UICollectionViewCell Snap to Centre

I have added a UICollectionView which will have 5 cells, scrolling horizontally. I would like the user to be able to scroll between the cells, where each cell will snap to the centre. Here is a my UICollectionFlowLayout code used with the cell sizes etc.
-(UICollectionViewFlowLayout*)collectionLayout{
if (!_collectionLayout) {
_collectionLayout = [[UICollectionViewFlowLayout alloc]init];
_collectionLayout.minimumInteritemSpacing = 0;
_collectionLayout.minimumLineSpacing = 30;
_collectionLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
_collectionLayout.itemSize = CGSizeMake(200, 165);
_collectionLayout.sectionInset = UIEdgeInsetsMake(0, 65, 0, 55);
_collectionLayout.collectionView.pagingEnabled = YES;
}
return _collectionLayout;
}
I have added insets so the first and last cell stops in the middle, though any of the three cells in between don't. Please see attached screen-shots to illustrate -[2 screen shots below]
I can easily achieve centralised paging of the cells if they are the width of the screen or I simply make 5 sections, however if I do this, then the user does not see the other cells left or right, which I need so they know there is more to scroll to, if you know what I mean.
I have read similar answers about disabling paging and using scrollViewWillEndDragging methods, but I could not get these to work either.
If anyone can offer any clear way to do this that would be great,
Thanks in advance
Jim
This likely won't be the solution you're ideally looking for, though it's an option you can use, as I have used for the set up you describe.
Increase your Uicollectionviewcell size to the width of the screen, with your purple square as view centralised within the cell. Remove your section inset code and change line spacing to =0.
At the left and right sides of the cell place arrow images (or as button), or a swipe gesture icon so app user knows there's more. As you've only got 5 cells, have cell at IndexPath.row==0 and IndexPath.row==4 to hide their left and right 'more' arrows respectively.

iOS message cell width/height using auto layout

The Goal
I'm trying to create a dynamic message cell using auto-layout.
What I've Tried
The cell is positioning correctly, for the most part, with auto-layout given the following constraints:
The Problem
My first problem was the message label (Copyable Label) width was constrained. That seems to be resolved by using setPreferredMaxLayoutWidth: as described in this question.
Height is still a problem. As you can see, the message bubble is still cutting off. In addition, I'm not sure how to determine the message cell height for the table view.
I expected auto-layout to somehow just work. I've read the answer here, but it seems like a lot to of steps.
The Question
First, is a case where auto-layout is more complex than traditional frame arithmetic?
Second, using auto-layout, how can I determine the height of the resulting cell?
I fully use Auto Layout and what you speak about is kinda a problem.
I didn't want to modify the way intrinsic size is calculated for performance purpose of UITable.
So I used a very simple way that is correct in the end. It's ok if your cell is simple, can become such hard if your cell contains more than one variable text.
I defined my cells normally, where you can put a UILabel that fits the insets (no problem about it).
Then, in your table datasource, you define directly the height of the cell:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [TEXTOFYOURCELL sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 1000)].height + 31; // Here it's defined for 15 of top and bottom insets, define +1 than the size of the cell is important.
}
EDIT :
Here some code about the UILabel in the cell (in init method).
__titleLabel = [UILabel new];
__titleLabel.numberOfLines = 0;
[self.contentView addSubview:__titleLabel]; // adding to contentView rather than self is very important !
[__titleLabel keepInsets:UIEdgeInsetsMake(0, 15, 0, 15)];
I use this API : https://github.com/iMartinKiss/KeepLayout to manage auto layout simpler.
This is possible on iOS 8 as can be read on AppCoda
Basically:
Set the label lines to 0.
Set the row height UITableViewAutomaticDimension

How to properly add custom TableViewCell?

I have a grouped tableView in my iPad-app, and I've been trying to set cell.imageView.center = cell.center to center the image instead of putting it to the leftmost position. This is apparently not possible without a subclass of the UITableviewCell(If someone could explain why, that'd also be appreciated.. For now I just assume they are 'private' variables as a Java-developer would call them).
So, I created a custom tableViewCell, but I only want to use this cell in ONE of the rows in this tableView. So in cellForRowAtIndexPath I basically write
cell = [[UITableViewCell alloc]initWith//blahblah
if(indexPath.row == 0)
cell = [[CustomCell alloc]initWith//blahblah
This is of course not exactly what I'm writing, but that's the idea of it.
Now, when I do this, it works, but the first cell in this GROUPED tableView turns out wider than the rest of them without me doing anything in the custom cell. The customCell class hasn't been altered yet. It still has rounded corners though, so it seems it knows it's a grouped tableView.
Also, I've been struggling with programmatically getting the size of a cell, in cellForRowAtIndexPath, I've tried logging out cell.frame.size.width and cell.contentView.frame.size.width, both of them returning 320, when I know they are a lot wider.. Like, all the rows are about 400 wide, and the first cell is 420 or something. It still writes out 320 for all the cells..
This code will not work for a couple of reasons:
cell.imageView.center = cell.center;
Firstly, the center is relative to its superview. I believe the cells superview is the tableView. The imageView's superview will be the content view of the cell. Therefore the coordinate systems are different so the centens will be offset. E.g. the 3rd cell down will have a center of 0.5 widths + 3.5 heights. You should be able to ge around this issue by doing:
cell.imageView.center = CGPointMake( width / 2 , height / 2 );
The second issue is related to how the table view works. The table view manages its cells view's. The width of a cell is defined by the table view's width and the height is defined by the table view's row height property. This means the cell itself has no control over its size.
You can however size its subviews, but you must do this after the cells size has been set (otherwise you can get strange results). You can do this in layout subviews (of the custom UITableViewCell class). See this answer.
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = ....
}
When layoutSubviews is called the cells frame has been set, so do your view logging here instead of cellForRowAtIndexpath.
As for the GROUPED style. Im not sure if this is designed to work with custom views. I suspect it sets the size of its cells to its own width minus a 20 pixel margin on each size, then applies a mask to the top and bottom cells in a section to get the rounded effect. If you are using custom view try to stick with a standard table view style.

Resources