How to configure a UICollectionView Cell Size per Size Class? - ios

I'm finding friction when trying to create responsive / adaptive UICollectionViewCells in the UIStoryboard.
The issue I'm seeing is that you don't seem to be able to set the Cell Size per Size Class and I'm trying to ascertain the right approach to this. I've designed the cells to adjust to their containers, so that they should autosize regardless of size class. This mostly works in that if I change the size class, select my cell view and do Update Frames then they all resize to fit their new size. However it's a one shot deal, if I go back to the Any/Any size class then I'm still seeing that resized version.
Here's what I'm aware I could try:
Create multiple cells, with fixed dimensions, one per size class and in the Storyboard view. I could then only use the right one at runtime but I could then see them at design time.
I could create a collection view Per Size class, each one only being installed for that size. This would work, but would be a pain to manage the multiple UICollectionViews
Create my interface and/or constraints programmatically (losing visibility of the design).
I'm hoping this is a solved scenario and I'm just missing something, but I'm aware that it could be that the IB tools don't match the code at this point.

The solution I came up with was just to implement the UICollectionViewDelegateFlowLayout and implement the sizeForItemAtIndexPath method.
This means that the cell dimensions can be set to match the available Size Class dimensions.
This still isn't ideal as you can't see the changes in the storyboard and you can't create a universal design and see it in each of the different formats.
I'm still hoping someone has a better option.

Here's a similar solution coded-out in Swift. I just styled both of my cells in the storyboard and leave them viewable for any size class combination. When the trait collection changes I update the cellSize and cellReuseID I want to use and tell the collectionView to reload all the visible cells. Then
collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
and
override func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
(not shown in sample code) are called which lets me update the size of the cell and update the cell's storyboard styling. So not entirely done in storyboard, but good enough until more support is provided in Xcode.
struct MyCollectionViewConstants{
static let CELL_ANY_ANY_REUSE_ID = "cell";
static let CELL_COMPACT_REGULAR_REUSE_ID = "cellSmall"
static let CELL_ANY_ANY_SIZE = 100;
static let CELL_COMPACT_REGULAR_SIZE = 70;
}
class MyCollectionView: UICollectionViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var cellSize = MyCollectionViewConstants.CELL_ANY_ANY_SIZE
var cellReuseID = MyCollectionViewConstants.CELL_ANY_ANY_REUSE_ID
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize{
return CGSize(width: cellSize, height: cellSize)
}
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact
&& self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.Regular){
cellSize = MyCollectionViewConstants.CELL_COMPACT_REGULAR_SIZE
cellReuseID = MyCollectionViewConstants.CELL_COMPACT_REGULAR_REUSE_ID
} else {
cellSize = MyCollectionViewConstants.CELL_ANY_ANY_SIZE
cellReuseID = MyCollectionViewConstants.CELL_ANY_ANY_REUSE_ID
}
self.collectionView.reloadItemsAtIndexPaths(
self.collectionView.indexPathsForVisibleItems())
}
}

I was stuck with same problem after implementing size class(iPad and iPhone).Well, I figured out a solution. Hope it helps!
Implement UICollectionViewDelegateFlowLayout.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
var device = UIDevice.currentDevice().model
var cellSize:CGSize = CGSizeMake(155, 109)
if (device == "iPad" || device == "iPad Simulator") {
cellSize = CGSizeMake(240, 220)
}
return cellSize
}

Swift 4
Hi Fellow Developers,
This is easy to do if the height and width of the UICollectionViewCell are same.
Steps
1. Import ** UICollectionViewDelegateFlowLayout**
2. Add the sizeForItem IndexPath method of UICOllectionView as follows
func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAt indexPath:IndexPath) -> CGSize {
return CGSize(width: collectionVw.frame.size.height, height: collectionVw.frame.size.height)
}
Note: What happening is you are setting the height of the UICollectionView as height and width of the UICollectionViewCell
Happy Coding :)

Related

UICollectionView items order not reversed in right to left languages

I noticed a big issue where in right to left languages, the cells order is not properly reversed, only the alignment is correct. But only for horizontal flow layout, and if the collection view contain different cell sizes! Yes, I know this sound insane. If all the cells are the same size, the ordering and alignment is good!
Here is what I got so far with a sample app (isolated to make sure this is the actual issue, and not something else):
(First bar should always be drawn blue, then size increase with index.)
Is this an internal bug in UICollectionViewFlowLayout (on iOS 11)? Or is there something obvious that I am missing?
Here is my test code (Nothing fancy + XIB with UICollectionView):
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "test", for: indexPath)
cell.backgroundColor = (indexPath.item == 0) ? UIColor.blue : UIColor.red
return cell
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (10 * indexPath.item) + 20, height: 170)
}
Automatic right-to-left support on dynamically-sized UICollectionViews is not a supported configuration. For this to work, you need to explicitly sign up for automatic layout mirroring as follows:
Create a subclass of UICollectionViewFlowLayout
Override flipsHorizontallyInOppositeLayoutDirection, and return true in Swift or YES in Objective-C
Set that as the layout of your collection view
This property is defined on UICollectionViewLayout (parent of Flow), so you can technically use this property on any custom layout you already have.
I believe that for this you will have to implement your own custom collectionViewLayout - although I understand that one would expect that it would automatically work just as the right-to-left on the rest of the components.

Determine UICollectionView cell height only by autolayout

I need a collection view which displays cells in a grid. So a standard flow layout is fine for me. However, I want to tell how many cells to show per row, while the cell height should be determined by the autolayout constraints that I put on the cell. Here is my cell layout:
It is quite simple - an image view and two labels below it. Now the image view has an aspect ratio constraint (1:1) which means whenever the width is known for the cell the height should automatically be known by the auto layout rules (there are vertical constraints going through: celltop-image-label1-label2-cellbottom).
Now, since I don't know any other good way to tell the collection view to show 2 items per row, I have overridden UICollectionViewDelegateFlowLayout methods:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
As you can see, since I don't know the item height I return the same thing as the width, hoping that the autolayout will fix it later. I also set the estimatedItemSize in order the whole mechanism to start working.
The results are quite strange - it seems like the collection view doesn't event take into account the width I return there, mostly depending on the label lengths:
I have seen some other answers where people recommend manually calculating the cell size for width, like telling "layout yourself, then measure yourself, then give me your size for this width", and even though it would still run the autolayout rules under the hood, I would like to know if there is a way of doing this without manually messing with the sizes.
You can easily find out the height of your collectionView cell in the storyboard's Size inspector, as shown below:
Now, just pick up this height from here, and pass it to the overridden UICollectionViewDelegateFlowLayout method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: **114**)
}
And you will get the desired output.
I ended up implementing a trick to move everything to autolayout. I completely removed the delegate method func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize and added a width constraint for my cell content in the interface builder (set the initial value to something, that's not important). Then, I created an outlet for that constraint in the custom cell class:
class MyCell: UICollectionViewCell {
#IBOutlet weak var cellContentWidth: NSLayoutConstraint!
func updateCellWidth(width: CGFloat) {
cellContentWidth.constant = width
}
}
Later, when the cell is created, I update the width constraint to the precalculated value according to the number of cells that I want per row:
private var cellWidth: CGFloat {
let paddingSpace = itemSpacing * (itemsPerRow - 1) + sectionInsets.left + sectionInsets.right
let availableWidth = collectionView.frame.width - paddingSpace
return availableWidth / itemsPerRow
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
...
cell.updateCellWidth(width: cellWidth)
return cell
}
And this, together with autolayout cell sizing enabled, will lay out the cells correctly.

Dynamically resizing UICollectionViewCell while editing content

I'm kind of stuck with a problem here.
So, I've got a UICollectionView with a textView inside. The problem is, that I need to change the collectionViewCell's height and therefor it's y-position in the collectionView if I'm adding a line of text to the textView. Unfortunately, everything I tried doesn't work. Here's what it looks like:
(I'm not allowed to post full size screenshots)
Green = collectionViewBackground, black = cell's background
Does somebody know how to make the green disappear?
Make your viewcontroller implements UICollectionViewDelegateFlowLayout
and override the sizeForItemAt function
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let data: String = list[indexPath.row] //Your data for specific index
let size = data.size(attributes: nil)
return size
}
This should help you resolve your problem and don't forget to set collectionview.delegate = self //Your view Controller

How can I get a UICollectionViewCell to be the entire width of the device?

I have a UIView inside my UICollectionViewCell that I need to be reproduced. I need this view to be pinned to the left and right of the screen. However, I can't find any way to control the width of the UICollectionViewCell.
Am I just missing something?
You're right. UICollectionViewCell doesn't play by the same AutoLayout rules as other views. Each cell is controlled by a layout class set on the UICollectionView.
Technically, you can drag the handles in Interface Builder and resize that way but it won't be dynamic for different screen widths and orientations.
I can think of two options:
Override the sizeForItemAtIndexPath: in the view controller and set to width of parent container programmatically. See Update 1 on question here for how this is done or check out the Apple documentation here for more background information
Consider using a UITableView if all cells will always be full width
You can use collectionView layouts. This code should help you:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var size = CGSize.zero
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
size = layout.itemSize;
size.width = collectionView.bounds.width
}
return size
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView.collectionViewLayout.invalidateLayout()
}

Variable width & height of UICollectionViewCell using AutoLayout with Storyboard (without XIB)

Here is an design issue in the app that uses AutoLayout, UICollectionView and UICollectionViewCell that has automatically resizable width & height depending on AutoLayout constraints and its content (some text).
It is a UITableView list like, with each cell that has it's own width & height calculated separately for each row dependant on its content. It is more like iOS Messages build in app (or WhatsUp).
It is obvious that app should make use of func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize.
Issue is that within that method, app cannot call func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell nor dequeueReusableCellWithReuseIdentifier(identifier: String, forIndexPath indexPath: NSIndexPath!) -> AnyObject to instantiate cell, populate it with specific content and calculate its width & height. Trying to do that will result in an indefinite recursion calls or some other type of app crash (at least in iOS 8.3).
The closest way to fix this situation seems to copy definition of the cell into view hierarchy to let Auto-layout resize "cell" automatically (like cell to have the same width as parent collection view), so app can configure cell with specific content and calculate its size. This should definitely not be the only way to fix it because of duplicated resources.
All of that is connected with setting UILabel.preferredMaxLayoutWidth to some value that should be Auto-Layout controllable (not hardcoded) that could depend on screen width & height or at least setup by Auto-layout constraint definition, so app can get multiline UILabel intrinsic size calculated.
I would not like to instantiate cells from XIB file since Storyboards should be today's industry standard and I would like to have as less intervention in code.
EDIT:
The basic code that cannot be run is listed bellow. So only instantiating variable cellProbe (not used) crashes the app. Without that call, app runs smoothly.
var onceToken: dispatch_once_t = 0
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
dispatch_once(&onceToken) {
let cellProbe = collectionView.dequeueReusableCellWithReuseIdentifier("first", forIndexPath: NSIndexPath(forRow: 0, inSection: 0)) as! UICollectionViewCell
}
return CGSize(width: 200,height: 50)
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("first", forIndexPath: NSIndexPath(forRow: 0, inSection: 0)) as! UICollectionViewCell
return cell
}
}
If you are using constraints, you don't need to set a size per cell. So you should remove your delegate method func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
Instead you need to set a size in estimatedItemSize by setting this to a value other than CGSizeZero you are telling the layout that you don't know the exact size yet. The layout will then ask each cell for it's size and it should be calculated when it's required.
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = someReasonableEstimatedSize()
Just like when dynamically size Table View Cell height using Auto Layout you don't need call
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
in
optional func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
the proper way is create a local static cell for height calculation like a class method of the custom cell
+ (CGFloat)cellHeightForContent:(id)content tableView:(UITableView *)tableView {
static CustomCell *cell;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cell = [tableView dequeueReusableCellWithIdentifier:[CustomCell cellIdentifier]];
});
Item *item = (Item *)content;
configureBasicCell(cell, item);
[cell setNeedsLayout];
[cell layoutIfNeeded];
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 1.0f; // Add 1.0f for the cell separator height
}
and I seriously doubt storyboard is some industry standard. xib is the best way to create custom cell like view including tableViewCell and CollectionViewCell
To calculate the CollectionView cell's size dynamically, just set the flow layout property to any arbitrary value:
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize(width: 0, height: 0)

Resources