Intrinsic content sized UICollectionView inside UITableView - ios

Let me preface by saying that I've spent probably 100 hours over the past few months searching Google and StackOverflow trying to answer this question. I've followed many tutorials and have not been able to solve my problem. Every one is a major disappointment and is disheartening.
Expectation
Reality
Problem
I have a UITableView that contains a UIView. The UIView has auto layout constraints to pin it 15pt off the edges of the cell, and then I give that a shadow and rounded corners. Inside that UIView, I have a few labels, then a UICollectionView, and a stack view.
My UICollectionView keeps having a height of 0 and I can't figure out how to make it have intrinsic size. The only way I can seem to make it display properly is to force a specific height to it via auto layout inside the storyboard (which is what I did to take this screenshot). Unfortunately, forcing a height like this is not realistic for 2 reasons:
The height varies based on the width of the device. Since the images are squares, the height is loosely (but not exactly) half the width of the device.
If there are no images, the height of the collection view should be 0.
It really seems like it should be possible to use the intrinsic content size of the flow layout here.
Storyboard
Constraints
I've got about 15pt of space between each of the labels, collection view, and stack view (so, the vertical space). The collection view prototype cell has an image view in it. The image view has top, bottom, trailing and leading space all set to 0 to the collection view cell, and also has a 1:1 ratio.
Collection View Layout
To achieve the layout of one large image and 4 small images, I'm using SNCollectionViewLayout.
Data Source / Delegate
In my controller, in viewDidLoad, I've set the estimatedRowHeight of the UITableView to 400, and I've set the rowHeight to UITableView.automaticDimension.
In the tableView cellForRowAt, I dequeue a reusable custom UITableViewCell class, set various outlets on the table view cell, and call reloadData() on the collection view for the table view cell. In the storyboard, I've set the table view cell as the collection view's data source and delegate.
In the table view cell's awakeFromNib function, I've got it setting the corner radius, shadow, and using the following code for the flow layout on the collection view (which you can see in their example):
let snCollectionViewLayout = SNCollectionViewLayout()
snCollectionViewLayout.fixedDivisionCount = 4
snCollectionViewLayout.delegate = self
snCollectionViewLayout.itemSpacing = 10
collectionView.collectionViewLayout = snCollectionViewLayout
I've also implemented the optional SNCollectionViewLayoutDelegate protocol by writing the following function:
func scaleForItem(inCollectionView collectionView: UICollectionView, withLayout layout: UICollectionViewLayout, atIndexPath indexPath: IndexPath) -> UInt {
if indexPath.row == 0 || collectionView.numberOfItems(inSection: indexPath.section) <= 2 {
return 2
} else {
return 1
}
}
This function is why the first image is twice the height and width as the rest.
Research
In trying to figure this out, I've checked the collectionView.collectionViewLayout.collectionViewContentSize inside the table view cell's cellForRowAt and willDisplay methods. In either case, it's (0,0).
I've also tried checking it in the collection view cell's cellForItemAt and willDisplay. Those log lines don't even print unless I add a height constraint to the collection view to force it tall, but then I don't have a good way to bring the height back down to what it should be, and any time I've tried, I got conflicting constraint warnings and really wonky stretched views (such as the stack view being totally squished).
In testing, I've forced a height constraint that's large enough for the collection view to fit, and when that happens, it renders correctly and the collection view content size is correct, so I know it's capable of keeping track (which can be seen here).
Does anyone have any insight into this? I'd be happy to provide more code if you have any specifics on things to look into. I tried to give the full picture, while keeping this as short as possible (I know it's long).
I feel like the issue has something to do with the collection view being inside a table view cell.
Thanks in advance.

Keep a height constraint connection in your table view cell.
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
And update the height constraint value programatically as per height calculations
self. collectionViewHeightConstraint.constant = 100 // set it to 0 if nothing to display

Related

Autosizing UICollectionView cell embedded in UITableView not working

I have spent a few day's now trying out things suggested on various SO posts but have not been able to figure this out. I'm probably looking right at the issue!
I am making a switch over from Android to iOS and seem to be having some trouble with autolayout. Specifically, getting the UICollectionView to resize to the height of its content. Each collection view can have a different cell size based on its content but the cell size will be the same for each item on a given row.
The UICollectionView is nested inside a UITableViewCell. The UITableViewCells appear to be resizing to its content height correctly and looking at the Debug View Hierarrchy the issue seems to be the UICollectionView.
I have created a mini demo and put it on this github (link below). I have thrown in a bunch of test data so the bottom part of the ViewController VC is a bit messy. The demo just illistrates how each row can have different content size. There can be several table rows of the same style.
NOTE: I had to add the following line to CategoryRowCell.swift in order for me to get the below screen shots. Without this line the UICollectionView height becomes 0!
...
collectionView.heightAnchor.constraint(equalToConstant: 250)
...
GitHub link
So here is what it looks like right now..
scrolled down
Notice the gaps at the top and bottom of the horizontal lists. Here is what i am expecting..
Here is part of the debug view hierarchy. You will see that the collection view already has top and bottom margins (good), but the red lines show the excess height being applied to the collection view. The green line indicates the height i am expecting. I am wanted the collection view to have a height of its content.
I think it would be better to do it the other way around, having the UICollectionView as the parent and UITableView as the child. Since the first is more flexible and customizable.
Anyway for your case I think you need to take a look at two things:
1- To have a better control of the cell size you can use this func for UICollectionView:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
// The desired size based on the value at that row (the name for example)
if objects[indexPath.row].name == "some condition" {
return CGSize(width: 150, height: 50)
}
else {
return CGSize(width: UIScreen.main.bounds.width, height: 75)
}
}
2- The size inspector values for the UICollectionView and UITableView (you might have some extra spacing in one of them)

Xcode image occupying whole tableView instead of cell

So I am creating a prototype which I will use later.
I have a table view with one cell, then I will make the whole table populate several cells (that is working)
My problem is, the image on my cell, instead of occupying the cell to occupies the whole table view as follows
Any ideas?
Thanks
Storyboard
Simulator
EDIT
repo: https://bitbucket.org/eduardoreecreate/coderswag-ios
Try setting the image view's content type to AspectFit and ClipToBounds to true
By default, an image view under the influence of auto layout wants to be the size of the image. Your image is big. Therefore the image view is big. Therefore the cell itself is big, because you are autosizing the cell height to match the height of its contents.
The simple solution is: don't do that! Before you put the image into the cell, munge it in code so that it is the correct height. It is very foolish to put a huge image into a small table cell in any case, or any small image view, because you're still wasting all that memory. Always try to crop / shrink the image before putting it into the interface.
More elaborately, you can give the image view an absolute height constraint to keep it from growing beyond a certain height. Or, as you've already been told, don't use automatic cell height; set the cell height to some absolute value by implementing heightForRow.
Implement heightForRowAt UITableView's delegate
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 150
// Customize or write a logic as per your requirement like for 1/4 of screen UIScreen.main.bounds.size.height / 4
}
If you select the cell in IB, then the Size Inspector shows Row Height at the top (with a "custom" checkbox), but if you select the whole table view the Size Inspector shows Row Height at the top there too (no "custom" in this case). The table view setting is used for dynamic cells. (Not sure if it's intentional)

Horizontal CollectionView with dynamic height cause FlowLayout warning

I have a side-scrolling collection view that contains two images. These images may be changed by the user during runtime. This change might cause a change of the collection view cell size, hence the collection view also need to adapt its height according to the height of the cells.
I've implemented the UICollectionViewDelegateFlowLayout protocol and the sizeForItemAt function which returns the proper cell size given the image.
I also have a height constraint outlet connected to my collection view which allows me to change its height, matching the height value of the chosen image.
It is all presented as you would expect when drawn to the screen, but I get the error "The behavior of the UICollectionViewFlowLayout is not defined because..", complaining that the height returned by sizeForItemAt exceeds my collection views default height (set in the storyboard).
It seems that I need to invalidate my collection view layout once I've altered its height constraint so that the collection view layout is updated with this new value before sizeForItemAt returns the cell sizes.
I've tried to call UICollectionViewLayouts invalidateLayout() right after updating the collection view height constraint, but that didn't help.
Any ideas to what I am doing wrong?
SOLVED
Ok, what I needed to do was to call view.layoutIfNeeded() for my view controller that holds the collection view right after I changed the collection views height constraint. Then the bounds for the collection view will be updated with the new height value so that the cells will fit.

Autolayout Width Constants in UICollectionViewCell doesn't work well

Below is a uicollectionviewcell, bar width must be according to number at right of it (vote count), event I log its width contants in console you can see it's calculated correctly, but on ui, two of them seems doesn't work well, if I scroll collectionview for several times it will be fixed, and then it will be fail again, how to set a view's contraint contant in a cell correctly?
code is too long but I want to summarize here
1) in viewcontroller's cellForItem
cell.setup()
cell.layoutIfNeeded()
return cell
2) in cell's setup()
setupBarWidths()
3) in setupBardWidhts()
resetAllWitdhs -> 0
calculate and set widths
is this correct way?
It depends upon how you are setting your constants for your subview inside uicollectionviewcell.
If your cell does not update properly constraints, possible solution is there
You must provide separate width constraint for your horizontal bar, and set it every time your reload cell.
"if I scroll collectionview for several times it will be fixed"
This problem occurs when you does not handle properly constraints changing, for example,you have "if - else " clause in your code, and you don't implement one branch.
Anyway, it's much easier to provide solution if you share your collectionView(_:cellForItemAt:)
UPDATED:
UICollectionView has many bugs internal, so one possible solution is to override UICollectionViewCell and insert the following method:
override var bounds: CGRect {
didSet {
contentView.frame = bounds
}
}
See this thread for more info.

UICollectionView scrolling in both directions

I made a UICollectionView with a vertical scroll.
The width of the cell is more than than the screen width, so I created a customFlowLayout based on UICollectionViewFlow layout returning the right calculated content size.
However, this doesn't work. When the width of the cell is less than the screen width it works. Does it mean that we can't have width more than than screen width in vertical scroll?
It is the same for horizontal scroll, but then the height of the CollectionView is limited to screen height.
Is there any way to make it work?
As others have already said, the UICollectionView can only scroll one direction using a flow layout. However you can accomplish this very easily without creating a custom layout or using a third party library.
When you lay your view out in story board, you can put your UICollectionView embedded in a UIScrollView. Have the scrollview set up to scroll horizontally and the UICollectionView to scroll Vertically. Then set the UICollectionView.delaysContentTouchesto true so touches will pass through to the UIScrollView and not think you are trying to scroll the collectionview.
When you set up the UICollectionView, set it's size and the size of the cells to be what you actually want them to be (Wider than the actual screen) and lay them out accordingly.
Now in the containing UIViewController put this code in the view lifecycle
- (void)viewDidLayoutSubviews {
self.myScrollView.contentSize = self.myCollectionView.frame.size;
}
That's literally all you have to do to accomplish what you are describing. Now your scrollview should allow you to scroll horizontally to view your entire cell and your collectionView should scroll vertically through your cells.
Happy programming.
I'm not sure I've understood your problem.
But if you have made a custom layout, make sure you have implemented :
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
and that your layout attributes frame and size are set with correct values for your "large cell" index path.
Also make sure you have implemented :
- (CGSize) collectionViewContentSize;
This method returns the contentSize of the collection View. If contentSize.width > youAppFrame.width you should have horizontal scrolling. Same for height and vertical scrolling.
Also make sure your collectionView allows scrolling and that your layout is prepared correctly using :
- (void)prepareLayout
By the way, for your layout have you overloaded UICollectionViewLayout or UICollectionViewFlowLayout ?
Before you can do that you MUST use a different type of layout. The flow layout represents its items as a list and it spans these items in cells based on the available width.
If you want to have both horizontal and vertical scrolling you need to somehow specify the number of columns for your grid. the FlowLayout doesn't have that. A simple sollution is to make a subclass of UICollectionViewLayout and override collectionViewContentSize to make it retun a width = to the added sum of the cells widths of one row (this is where knowing how many collumns you want is necessary), plus any additional spacing between them. This will work fine if your cells have the same size per column, similar to a grid.
You should embed a UITableView into a UIScrollView.
ScrollView and TableView will have the same height but different widths.
This way UITableView will scroll vertical and UIScrollView will scroll horizontal.
Xcode 11+, Swift 5.
I solved my issue, I prepared video and code

Resources