UICollectionView inside UITableViewCell default contentOffset - ios

In my app I have a tableView which contains several cells with UICollectionView, The layout of UICollectionView is custom and set in cellForRowAtIndexPath.
In prepare method of collectionViewLayout I set a default contentOffset to collectionView.
But this contentOffset only works for visible table cells and when I scroll tableview other cells do not have this default content offset.
How to fix this issue ?
override func prepare() {
guard cache.isEmpty, let collectionView = collectionView else {
return
}
// ...
// Prepare cell attributes and add to cache array
// ...
collectionView.contentOffset = CGPoint(x: 100, y:0)
}

I used the following solution to fix this issue, Set content offset after a few milliseconds delay fixed this issue.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(10), execute: {
collectionView.contentOffset = self.initialContentOffset!
})
But I think this isn't the best solution but I couldn't find another solution to fix this issue

Related

Update cell height after it is rendered

I've set up a UITableViewCell with UITableViewAutomaticDimension
The TableViewCell has a UICollectionView embedded in it which is not scrollable but can have a variable height based on the content of the collectionview.
Right now what I've tried is the render the cell and assign the height constraint of the collectionview to a variable collectionViewHeightConstraint and then update the height once the collectionview is rendered in the layoutSubviews method
override func layoutSubviews() {
super.layoutSubviews()
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
}
This is what the collectionview constraints look like (using cartography) :
self.contentView.addSubview(self.collectionview)
self.contentView.addSubview(self.holdingView)
constrain(self.holdingView!, self.contentView) {
holderView, container in
holderView.top == container.top
holderView.left == container.left
holderView.right == container.right
holderView.bottom == container.bottom
}
constrain(self.collectionView!, self.holdingView) {
collectionView, containerView in
collectionView.top == containerView.top
collectionView.left == containerView.left
collectionView.right == containerView.right
collectionViewHeightConstraint = collectionView.height == collectionViewHeight
containerView.bottom == collectionView.bottom
}
But that does not seem to update the cell height.
Is there any way to update the same?
Edit
This is not a duplicate question as suggested by some people and the explanation of why is in the comments below.
Since the comment was too small a space, I'll put everything here:
Note: You don't actually have to set the height constraint in viewDidLayoutSubviews just somewhere you can be sure that the UICollectionView has been set and your layout has been setup properly on your whole screen! For example, doing it in viewDidAppear and then calling layoutIfNeeded() will also work. Moving it into viewDidAppear will only work if you have your UICollectionView setup before viewDidAppear is called i.e you know your UICollectionView dataSource beforehand.
Fix 1:
Try reloading the UITableView after setting the height and checking if the heightConstant != contentSize. Use this to check if the height of the UICollectionView is updated properly i.e.:
override func layoutSubviews() {
super.layoutSubviews()
if self.collectionViewHeightConstraint?.constant != self.collectionView!.contentSize.height{
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
//to make sure height is recalculated
tableView.reloadData()
//or reload just the row depending on use case and if you know the index of the row to reload :)
}
}
I agree with your comment and that it is messy, I meant use that as a fix and/or to check if that is where the problem lies actually!
As for why it is 0, that happens probably because your UICollectionView hasn't been set yet (cellForItem hasn't been called yet) so contentSize isn't actually calculated!
Fix 2:
Once your dataSource for the UICollectionView has been set, that is you receive the data, you calculate the height the UICollectionView contentSize will have manually and set it once and reload the row. If the calculation is a tedious task, just set the dataSource and call reloadData on UICollectionView. This will ensure the UICollectionView is setup properly and then set the constraint of the cell to be the contentSize and call reloadData or reloadRow on the UITableView.
You basically can set the heightConstraint anytime after your UICollectionView has been setup and your view has been laid out. You just need to called tableView.reloadData() afterwards.
You can reload particular cell of tableview
let indexPath = IndexPath(item: rowNumber, section: 0)
tableView.reloadRows(at: [indexPath], with: .top)
Going by your requirement, I guess if we load the collectionview first and then load the tableview with the correct height of the collectionview, we can solve this.
collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
})
tableview.reloadData()
By this when tableview loads the cell has the desired height based on the collection view content size.

iOS 10 bug: UICollectionView received layout attributes for a cell with an index path that does not exist

Running my app in a device with iOS 10 I get this error:
UICollectionView received layout attributes for a cell with an index path that does not exist
In iOS 8 and 9 works fine. I have been researching and I have found that is something related to invalidate the collection view layout. I tried to implement that solution with no success, so I would like to ask for direct help. This is my hierarchy view:
->Table view
->Each cell of table is a custom collection view [GitHub Repo][1]
->Each item of collection view has another collection view
What I have tried is to insert
[self.collectionView.collectionViewLayout invalidateLayout];
In the
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
of both collection views.
Also I have tried to invalidate layout before doing a reload data, does not work...
Could anyone give me some directions to take?
This happened to me when number of cells in collectionView changed. Turns out I was missing invalidateLayout after calling reloadData. After adding it, I haven't experienced any more crashes. Apple has made some modifications to collectionViews in iOS10. I guess that's the reason why we are not experiencing same problem on older versions.
Here's my final code:
[self.collectionView reloadData];
[self.collectionView.collectionViewLayout invalidateLayout];
//Swift 4.2 Version
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
When you have custom layout, remember to clear cache (UICollectionViewLayoutAttributes) while overriding prepare func
override func prepare() {
super.prepare()
cache.removeAll()
}
I also faced this issue with 2 collectionViews in the same view because I added the same UICollectionViewFlowLayout in both collections :
let layout = UICollectionViewFlowLayout()
collectionView1.collectionViewLayout = layout
collectionView2.collectionViewLayout = layout
Of course it crashes on reload data if collectionView1 Data change. If this could help someone.
So you should do this instead
let layout1 = UICollectionViewFlowLayout()
collectionView1.collectionViewLayout = layout1
let layout2 = UICollectionViewFlowLayout()
collectionView2.collectionViewLayout = layout2
Previous answer helps, but if you use autoresizing cells, their size will be incorrect.
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.estimatedItemSize = CGSizeMake(60, 25);
layout.itemSize = UICollectionViewFlowLayoutAutomaticSize;
self.collectionView.collectionViewLayout = layout;
I solve this issues by replacing
[self.collectionView reloadData];
to
[self.collectionView reloadSections:indexSet];
The #Katrin's answer helped a lot, but I could achieve even better results by adding one more line:
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutSubviews() // <-- here it is :)
I can't now say if I could reproduce crash with this line or not, but I guess there was one... So, still not a silver bullet, but something.
It's not the best to reloadData everytime (You should use insertItems and deleteItems, and even reloadSections).
But... after saying that in some cases it's a valid so, you can actually do this:
collectionView.dataSource = nil
collectionView.delegate = nil
/*... All changes here. ... */
collectionView.dataSource = self
collectionView.delegate = self
collectionView.reloadData()
Resetting UICollectionView's layout solved the problem for me:
let layout = UICollectionViewFlowLayout()
collectionView?.collectionViewLayout = layout
Calling invalidateLayout did not prevent the crash in my case. (It worked if the number of items in the collection view increased but not if it decreased). Context: I have a UICollectionView inside a UITableViewCell and when the table cell is re-used I reset the delegates of the collectionView. I fixed the problem not by invalidating the cache but by RECREATING the layout object any time I reset the delegate, then calling reloadData():
foo.dataSource = newDataSource
foo.delegate = newDelegate
foo.fixLayoutBug()
foo.reloadData()
func fixLayoutBug() {
let oldLayout = collectionViewLayout as! UICollectionViewFlowLayout
let newLayout = UICollectionViewFlowLayout()
newLayout.estimatedItemSize = oldLayout.estimatedItemSize
newLayout.footerReferenceSize = oldLayout.footerReferenceSize
newLayout.headerReferenceSize = oldLayout.headerReferenceSize
newLayout.itemSize = oldLayout.itemSize
newLayout.minimumInteritemSpacing = oldLayout.minimumInteritemSpacing
newLayout.minimumLineSpacing = oldLayout.minimumLineSpacing
newLayout.scrollDirection = oldLayout.scrollDirection
newLayout.sectionFootersPinToVisibleBounds = oldLayout.sectionFootersPinToVisibleBounds
newLayout.sectionHeadersPinToVisibleBounds = oldLayout.sectionHeadersPinToVisibleBounds
newLayout.sectionInset = oldLayout.sectionInset
newLayout.sectionInsetReference = oldLayout.sectionInsetReference
collectionViewLayout = newLayout
}
In my case, I was changing the NSlayoutConstraint constant
self.collectionViewContacts.collectionViewLayout.invalidateLayout()
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
self.collectionViewContacts.reloadData()
solved the issue
I had the problem using RxDataSources with RxCollectionViewSectionedAnimatedDataSource and solved it by combining two answers. I have to invalidateLayout() when reactively reload my collection:
...
.asDriver(onErrorJustReturn: [])
.do(onNext: { [weak self] _ in
DispatchQueue.main.async {
self?.collectionViewLayout.invalidateLayout()
self?.collectionView.layoutSubviews()
}
}).drive(collectionView.rx.items(dataSource: collectionController.dataSource))
.disposed(by: disposeBag)
and clear cache array(s) in prepare() layout method. Here is the code:
override func prepare() {
super.prepare()
cache.removeAll()
guard let collectionView = collectionView,
collectionView.numberOfSections > 0,
let delegate = delegate else {
return
}
...
If you want to keep your scroll position and fix the crash you can use this:
collectionView.reloadData()
let context = collectionView.collectionViewLayout.invalidationContext(forBoundsChange: collectionView.bounds)
context.contentOffsetAdjustment = CGPoint.zero
collectionView.collectionViewLayout.invalidateLayout(with: context)
I faced the similar issue while showing-hiding collection view using animated constraints change. My solution is based on existing answers.
self.filtersCollectionView.reloadData()
if isNeedToUpdateConstraint {
filtersCollectionViewHeightLayoutConstraint.constant = updatedHeight
UIView.animate(withDuration: 0.1, animations: {
self.view.setNeedsLayout()
}, completion: { completion in
if completion {
self.filtersCollectionView.collectionViewLayout.invalidateLayout()
}
})
}
I had this problem as well. In my case there was a custom UICollectionViewLayout applied to the collection view & the problem was that it had stale data for the number of cells that should be displayed. That's definitely something you can check on when you see UICollectionView received layout attributes for a cell with an index path that does not exist
I managed to solve this by recreating the collectionViewLayout before calling reloadData()
The issue was probably related to me having a separate instance for the dataSource (i.e. not the view controller holding the collectionView), and when swapping datasource, the crash could appear.
This happened to me as well, but it was because my UICollectionViewDataSource changed, and I didn't call -[UICollectionView reloadData]. In my case, I had the following data structure:
struct Bar { let name: String }
struct Foo { let name: String; let bars: [Bar] }
I had two UICollectionViews: one for Foos and one for Bars. In my -collectionView:didSelectItemAtIndexPath:, I had the following:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.fooCollectionView) {
self.selectedFoo = self.foos[indexPath.row];
[self.barCollectionView reloadData];
self.fooCollectionView.hidden = YES;
self.barCollectionView.hidden = NO;
} else if (collectionView == self.barCollectionView) {
// do some stuff with the selected bar
self.selectedFoo = nil;
// This -reloadData call fixed my error. I thought I didn't
// need it since my UICollectionView was hidden
[self.barCollectionView reloadData];
self.barCollectionView.hidden = YES;
self.fooCollectionView.hidden = NO;
}
}
Without the -reloadData call, I would see the crash when I rotated the device.
After spending two days of time, the below code solved the issue. Before loading a collectionview write the below code.
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
collecView.collectionViewLayout = flowLayout
collecView.delegate = self
collecView.dataSource = self
collecView.reloadData()
Then the crash will be resolved, but you will find issue in collectionview cells, as the cells are compressed or not as per your design. Then just a line of code after scrolldirection line
flowLayout.itemSize = CGSize(width: 92, height: 100)
With the above line of code, you can adjust you collectionview cell layout.
I hope it will help someone.
In my case I was changing the constant of NSLayoutConstraint none of the above solution worked for me. thanks to #jeff ayan whose answer gave me the hint. I solved the problem with this code
override func prepareForReuse() {
super.prepareForReuse()
imagesCollectionHeight.constant = 100 // changing collectionview height constant
imageContainerWidth.constant = 100 //changing width constant of some view
self.layoutIfNeeded() // this line did the trick for me
}
Hope it helps someone
The following made it for me:
[self.collectionView reloadData];
[self.collectionView layoutIfNeeded];
This code seems to force collectionView to scroll to the first cell before reloading data, what seems to cause the error in my case.

UICollectionView is not allowing full scroll --- last few rows are cut off

I have a UICollectionView that holds a bunch of a photos.
However, if I scroll to the bottom the scrollview does not let me scroll to the bottom of the last few rows (it snaps back). I have tried override the collectionView.contentSize and just adding 1000 to the height but it doesn't fix the problem.
collectionView.contentSize = CGSizeMake(collectionView.contentSize.width, collectionView.contentSize.height + 1000)
Here is a video of the problem:
https://www.youtube.com/watch?v=fH57_pL0OjQ&list=UUIctdpq1Pzujc0u0ixMSeVw
Here is my code to create cells:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("selectPhotoCell", forIndexPath: indexPath) as B_SelectPhotoControllerViewCell
let reuseCount = ++cell.reuseCount
let asset = currentAssetAtIndex(indexPath.item)
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize:_cellSize, contentMode: .AspectFit, options: nil)//the target size here can be set to CGZero for a super blurry preview
{
result, info in
if reuseCount == cell.reuseCount
{
cell.imageView.image = result
cell.imageView.frame = CGRectMake(0, 0,self._cellSize.width,self._cellSize.height)
}
}
return cell
}
private func currentAssetAtIndex(index:NSInteger)->PHAsset
{
if let fetchResult = _assetsFetchResults
{
return fetchResult[index] as PHAsset
}else
{
return _selectedAssets[index]
}
}
Update:
Because I am adding this as a child view controller, there seems to be some problems with the offsetting of the scrollview. I haven't fixed it yet but when open this view without adding it as a child view to another view controller, the scrollview is the correct size
The problem was I was adding this as a child view controller.
As a result, after doing some animations, the UICollectionView bounds were sizing to the view it was attached to. As a result its height was wrong and hence why it was getting cut off.
I just came across this question from a quick Google and felt I could add something useful.
I am running a segmentedControl with a UIView that changes to different UICollectionViews on the segment change and I couldn't get the collectionView to scroll fully down.
This may not be the solution for all, but I found that if I went to the XIB I was loading in the view and set size to freeform and decrease it by the size of a cell I had removed the problem.
suspect your CollectionView's bottomAchor was not set correctly to the parent uiview's safeAreaLayoutGuide bottomAnchor

UICollectionView inside a UITableViewCell -- dynamic height?

One of our application screens requires us to place a UICollectionView inside of a UITableViewCell. This UICollectionView will have a dynamic number of items, resulting in a height which must be calculated dynamically as well. However, I am running into problems trying to calculate the height of the embedded UICollectionView.
Our overarching UIViewController was created in Storyboards and does make use of auto layout. But, I don't know how to dynamically increase the height of the UITableViewCell based on the height of the UICollectionView.
Can anyone give some tips or advice on how to accomplish this?
The right answer is YES, you CAN do this.
I came across this problem some weeks ago. It is actually easier than you may think. Put your cells into NIBs (or storyboards) and pin them to let auto layout do all the work
Given the following structure:
TableView
TableViewCell
CollectionView
CollectionViewCell
CollectionViewCell
CollectionViewCell
[...variable number of cells or different cell sizes]
The solution is to tell auto layout to compute first the collectionViewCell sizes, then the collection view contentSize, and use it as the size of your cell. This is the UIView method that "does the magic":
-(void)systemLayoutSizeFittingSize:(CGSize)targetSize
withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority
verticalFittingPriority:(UILayoutPriority)verticalFittingPriority
You have to set here the size of the TableViewCell, which in your case is the CollectionView's contentSize.
CollectionViewCell
At the CollectionViewCell you have to tell the cell to layout each time you change the model (e.g.: you set a UILabel with a text, then the cell has to be layout again).
- (void)bindWithModel:(id)model {
// Do whatever you may need to bind with your data and
// tell the collection view cell's contentView to resize
[self.contentView setNeedsLayout];
}
// Other stuff here...
TableViewCell
The TableViewCell does the magic. It has an outlet to your collectionView, enables the auto layout for collectionView cells using estimatedItemSize of the UICollectionViewFlowLayout.
Then, the trick is to set your tableView cell's size at the systemLayoutSizeFittingSize... method. (NOTE: iOS8 or later)
NOTE: I tried to use the delegate cell's height method of the tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.but it's too late for the auto layout system to compute the CollectionView contentSize and sometimes you may find wrong resized cells.
#implementation TableCell
- (void)awakeFromNib {
[super awakeFromNib];
UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// Configure the collectionView
flow.minimumInteritemSpacing = ...;
// This enables the magic of auto layout.
// Setting estimatedItemSize different to CGSizeZero
// on flow Layout enables auto layout for collectionView cells.
// https://developer.apple.com/videos/play/wwdc2014-226/
flow.estimatedItemSize = CGSizeMake(1, 1);
// Disable the scroll on your collection view
// to avoid running into multiple scroll issues.
[self.collectionView setScrollEnabled:NO];
}
- (void)bindWithModel:(id)model {
// Do your stuff here to configure the tableViewCell
// Tell the cell to redraw its contentView
[self.contentView layoutIfNeeded];
}
// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place,
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {
// With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
[self.collectionView layoutIfNeeded];
// If the cell's size has to be exactly the content
// Size of the collection View, just return the
// collectionViewLayout's collectionViewContentSize.
return [self.collectionView.collectionViewLayout collectionViewContentSize];
}
// Other stuff here...
#end
TableViewController
Remember to enable the auto layout system for the tableView cells at your TableViewController:
- (void)viewDidLoad {
[super viewDidLoad];
// Enable automatic row auto layout calculations
self.tableView.rowHeight = UITableViewAutomaticDimension;
// Set the estimatedRowHeight to a non-0 value to enable auto layout.
self.tableView.estimatedRowHeight = 10;
}
CREDIT: #rbarbera helped to sort this out
I think my solution is much simpler than the one proposed by #PabloRomeu.
Step 1. Create outlet from UICollectionView to UITableViewCell subclass, where UICollectionView is placed. Let, it's name will be collectionView
Step 2. Add in IB for UICollectionView height constraint and create outlet to UITableViewCell subclass too. Let, it's name will be collectionViewHeight.
Step 3. In tableView:cellForRowAtIndexPath: add code:
// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
Both table views and collection views are UIScrollView subclasses and thus don't like to be embedded inside another scroll view as they try to calculate content sizes, reuse cells, etc.
I recommend you to use only a collection view for all your purposes.
You can divide it in sections and "treat" some sections' layout as a table view and others as a collection view. After all there's nothing you can't achieve with a collection view that you can with a table view.
If you have a basic grid layout for your collection view "parts" you can also use regular table cells to handle them. Still if you don't need iOS 5 support you should better use collection views.
I read through all the answers. This seems to serve all cases.
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
return collectionView.collectionViewLayout.collectionViewContentSize
}
Pablo Romeu's answer above (https://stackoverflow.com/a/33364092/2704206) helped me immensely with my issue. I had to do a few things differently, however, to get this working for my problem. First off, I didn't have to call layoutIfNeeded() as often. I only had to call it on the collectionView in the systemLayoutSizeFitting function.
Secondly, I had auto layout constraints on my collection view in the table view cell to give it some padding. So I had to subtract the leading and trailing margins from the targetSize.width when setting the collectionView.frame's width. I also had to add the top and bottom margins to the return value CGSize height.
To get these constraint constants, I had the option of either creating outlets to the constraints, hard-coding their constants, or looking them up by an identifier. I decided to go with the third option to make my custom table view cell class easily reusable. In the end, this was everything I needed to get it working:
class CollectionTableViewCell: UITableViewCell {
// MARK: -
// MARK: Properties
#IBOutlet weak var collectionView: UICollectionView! {
didSet {
collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
selectionStyle = .none
}
}
var collectionViewLayout: UICollectionViewFlowLayout? {
return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
}
// MARK: -
// MARK: UIView functions
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)
let size = collectionView.collectionViewLayout.collectionViewContentSize
let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
return newSize
}
}
As a helper function to retrieve a constraint by identifier, I add the following extension:
extension UIView {
func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
return constraints.first(where: { $0.identifier == identifier })
}
}
NOTE: You will need to set the identifier on these constraints in your storyboard, or wherever they are being created. Unless they have a 0 constant, then it doesn't matter. Also, as in Pablo's response, you will need to use UICollectionViewFlowLayout as the layout for your collection view. Finally, make sure you link the collectionView IBOutlet to your storyboard.
With the custom table view cell above, I can now subclass it in any other table view cell that needs a collection view and have it implement the UICollectionViewDelegateFlowLayout and UICollectionViewDataSource protocols. Hope this is helpful to someone else!
An alternative to Pablo Romeu's solution is to customise UICollectionView itself, rather than doing the work in table view cell.
The underlying problem is that by default a collection view has no intrinsic size and so cannot inform auto layout of the dimensions to use. You can remedy that by creating a custom subclass which does return a useful intrinsic size.
Create a subclass of UICollectionView and override the following methods
override func intrinsicContentSize() -> CGSize {
self.layoutIfNeeded()
var size = super.contentSize
if size.width == 0 || size.height == 0 {
// return a default size
size = CGSize(width: 600, height:44)
}
return size
}
override func reloadData() {
super.reloadData()
self.layoutIfNeeded()
self.invalidateIntrinsicContentSize()
}
(You should also override the related methods: reloadSections, reloadItemsAtIndexPaths in a similar way to reloadData())
Calling layoutIfNeeded forces the collection view to recalculate the content size which can then be used as the new intrinsic size.
Also, you need to explicitly handle changes to the view size (e.g. on device rotation) in the table view controller
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
Easiest approach I've came up with, so far, Credits to #igor answer above,
In your tableviewcell class just insert this
override func layoutSubviews() {
self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}
and of course, change the collectionviewoutlet with your outlet in the cell's class
I was facing the same issue recently and I almost tried every solution in the answers, some of them worked and others didn't my main concern about #PabloRomeu approach is that if you have other contents in the cell (other than the collection view) you will have to calculate their heights and the heights of their constraints and return the result to get the auto layout right and I don't like to calculate things manually in my code. So here is the solution that worked fine for me without doing any manual calculations in my code.
in the cellForRow:atIndexPath of the table view I do the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
//initialize the the collection view data source with the data
cell.frame = CGRect.zero
cell.layoutIfNeeded()
return cell
}
I think what happens here is that I force the tableview cell to adjust its height after the collection view height has been calculated. (after providing the collectionView date to the data source)
I would put a static method on the collection view class that will return a size based on the content it will have. Then use that method in the heightForRowAtIndexPath to return the proper size.
Also note that you can get some weird behavior when you embed these kinds of viewControllers. I did it once and had some weird memory issues I never worked out.
Maybe my variant will be useful; i've been deciding this task during last two hours. I don't pretend it's 100% correct or optimal, but my skill's very small yet and i'd like to hear comments from experts. Thank you.
One important note: this works for static table - it's specified by my current work.
So, all I use is viewWillLayoutSubviews of tableView. And a little bit more.
private var iconsCellHeight: CGFloat = 500
func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
UIView.animateWithDuration(duration, animations: { () -> Void in
table.beginUpdates()
table.endUpdates()
})
}
override func viewWillLayoutSubviews() {
if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
if collectionViewContentHeight + 17 != iconsCellHeight {
iconsCellHeight = collectionViewContentHeight + 17
updateTable(tableView, withDuration: 0.2)
}
}
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch (indexPath.section, indexPath.row) {
case ...
case (1,0):
return iconsCellHeight
default:
return tableView.rowHeight
}
}
I know, that the collectionView is located in the first row of the second section;
Let the height of the row is 17 p. bigger, than its content height;
iconsCellHeight is a random number as the program starts (i know, that in the portrait form it has to be exactly 392, but it's not important). If the content of collectionView + 17 is not equal this number, so change its value. Next time in this situation the condition gives FALSE;
After all update the tableView. In my case its the combination of two operations (for nice updating of extending rows);
And of course, in the heightForRowAtIndexPath add one row to code.
I get idea from #Igor post and invest my time to this for my project with swift
Just past this in your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
cell.frame = tableView.bounds
cell.layoutIfNeeded()
cell.collectionView.reloadData()
cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
cell.layoutIfNeeded()
return cell
}
Addition:
If you see your UICollectionView choppy when loading cells.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//do dequeue stuff
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
return cell
}
Pablo's solution did not work very well for me, I had strange visual effects (the collectionView not adjusting correctly).
What worked was to adjust the height constraint of the collectionView (as a NSLayoutConstraint) to the collectionView contentSize during layoutSubviews(). This is the method called when autolayout is applied to the cell.
// Constraint on the collectionView height in the storyboard. Priority set to 999.
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {
collectionViewHeightConstraint.constant = collectionView.contentSize.height
super.layoutSubviews()
}
// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
layoutIfNeeded()
}
func configure(data: [Strings]) {
names = data
contentView.layoutIfNeeded()
collectionviewNames.reloadData()
}
Short and sweet. Consider the above method in your tableViewCell class. You would probably call it from func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell after dequeing your cell. Before calling reloadData on your collection view, in your tableCell, you need to tell the collection view to lay out its subviews, if layout updates are pending.
In your UITableViewDelegate:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return ceil(itemCount/4.0f)*collectionViewCellHeight;
}
Substitute itemCount and CollectionViewCellHeight with the real values. If you have an array of arrays itemCount might be:
self.items[indexPath.row].count
Or whatever.
1.Create dummy cell.
2.Use collectionViewContentSize method on UICollectionViewLayout of UICollectionView using current data.
You can calculate the height of the collection based on its properties like itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, if your collectionViewCell has the border of a rule.

Autoresize UICollectionView height to adjust to its content size

I have a UICollectionView, a button that creates a new cell in collection view. And I want UICollectionView to adjust it's size according to it's content size (when there are one or two cells then UICollectionView is short, if there are a lot of cell UICollectionView is big enough).
I know how to get content size:
collectionView.collectionViewLayout.collectionViewContentSize
But I have no idea where to use this value. I would appreciate if somebody help me to figure out how to make UICollectionView auto adjust it's height.
UPD:
I published on GitHub a demo project that describes the problem: https://github.com/avokin/PostViewer
I don't think content size is what you're after. I think you're wanting to adjust the amount of screen real estate consumed by the collection view, right? That's going to require adjustment of the frame. The content size includes the off-screen (scrolling) area as well as the on screen view.
I don't know of anything that would prevent you from just changing the frame size on the fly:
collectionView.frame = CGRectMake (x,y,w,h);
[collectionView reloadData];
If I'm understanding you correctly.
Use a height constraint for the collection view and update its value with the content height when needed. See this answer: https://stackoverflow.com/a/20829728/3414722
Steps to change the UICollectionView frame:
Set the "translatesAutoresizingMaskIntoConstraints" property to YES for the collectioview's superview (If you are using AUTOLAYOUT)
Then update the collectioview's frame as :
collectionView.frame = CGRectMake (x,y,w,h);
[collectionView reloadData];
You need to constrain the collection view height to the height of your content:
I'm using SnapKit in the following code.
First constrain the collection view edges to its superview:
private func constrainViews() {
collectionView?.translatesAutoresizingMaskIntoConstraints = true
collectionView?.snp.makeConstraints { make in
make.edges.equalToSuperview()
heightConstraint = make.height.equalTo(0).constraint
}
}
Next calculate the height and set the height to the height constraint offset. I'm letting the flow layout do the work, and then calculating the height based on the bottom edge of the last layout attribute:
override func viewDidLayoutSubviews() {
guard
let collectionView = collectionView,
let layout = collectionViewLayout as? UICollectionViewFlowLayout
else {
return
}
let sectionInset = layout.sectionInset
let contentInset = collectionView.contentInset
let indexPath = IndexPath(item: tags.count, section: 0)
guard let attr = collectionViewLayout.layoutAttributesForItem(at: indexPath) else {
return
}
// Note sectionInset.top is already included in the frame's origin
let totalHeight = attr.frame.origin.y + attr.frame.size.height
+ contentInset.top + contentInset.bottom
+ sectionInset.bottom
heightConstraint?.update(offset: totalHeight)
}
Note that in the example, I always have one special tag not included in my items tags count, so the line:
let indexPath = IndexPath(item: tags.count, section: 0)
would need to be something like if items.count > 0 ... let indexPath = IndexPath(item: tags.count - 1, section: 0) in most other code.

Resources