Flexible size UICollectionview Footer not working properly - ios

I have been working on a UI screen where I need to fill up screen with fetched data from API and show view at very bottom of the screen.
My screen contains data in scrollable fashion so I have to use UICollectionView to manage this scrollable content properly.
i.e. If more content then footer will be shown at end of UICollectionView after whole list scroll is over, if less content then footer will be show at very bottom of the screen leaving blank space between footer and content.
Following is my code where I have dynamic flow layout which changes based on data I get.
API Function:
private func fetchEventDetails(_ eventId: Int) {
{
API Call - Success
}
self.contentItem = response
} onFailure: { error in
{
API Call - Failure
}
}
ContentItem is a variable stores API data and loads them into UICollectionView. So, whenever contentItem is set, new flowLayout is generated and Collection view is being re-rendered.
var contentItem: LSTEventDetails? {
didSet {
updateCollectionViewLayout()
}
}
private func updateCollectionViewLayout() {
CollectionView.collectionViewLayout = <generateNewFlowLayout()>
CollectionView.reloadData()
CollectionView.collectionViewLayout.invalidateLayout()
CollectionView.layoutIfNeeded()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
Above function assigns new Flow layout to collection view and we have written resize footer view related function in viewDidLayoutSubviews, so, layout methods are called here. I don't know best place where to call this resizeFooter function which can be executed once data is loaded.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if CollectionView.numberOfSections > 0 {
//DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.CollectionView.resizeStickyFooter(for: IndexPath(item: 0, section: 0), andSectionLayout: self.sectionLayouts, andView: self.view)
//}
}
}
In the above function, if I add asyncAfter I get some jerky animation, like all data is loaded and then after sometime, the footer jumps to correct position which seems a bit weird.
sectionLayouts holds reference to all NSCollectionLayoutSection sections which I create while generating flow layout.
The main resizeFooter function is added as extension to UICollectionView:
extension UICollectionView {
func resizeStickyFooter(for indexPath: IndexPath = IndexPath(item: 0, section: 0), andSectionLayout sectionLayouts: [NSCollectionLayoutSection]?, andView view: UIView) {
let size = self.bounds.size.height - self.contentInset.bottom//- (sectionLayouts?.reduce(0, { $0 + $1.contentInsets.top + $1.contentInsets.bottom }) ?? 0) // - LSTTheme.UI.TabBarSize - no need as tab is hidden
let layoutViewSize = self.collectionViewLayout.collectionViewContentSize
if layoutViewSize.height < size {
if let footerView = self.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath) {
let footerHeight = size - layoutViewSize.height
let footerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(max(0, footerHeight) + footerView.bounds.height)
)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: footerSize,
elementKind: UICollectionView.elementKindSectionFooter,
alignment: .bottom
)
if (indexPath.section < (sectionLayouts?.count ?? 0)),
let layouts = sectionLayouts {
let section = layouts[indexPath.section]
section.boundarySupplementaryItems.removeAll(where: {$0.elementKind == UICollectionView.elementKindSectionFooter} )
section.boundarySupplementaryItems.append(sectionFooter)
let layout = UICollectionViewCompositionalLayout { sectionIndex, env in
return layouts[sectionIndex]
}
self.collectionViewLayout = layout
self.collectionViewLayout.invalidateLayout()
self.layoutIfNeeded()
}
}
}
}
}
Here, when this function gets executed at first time, I am not getting proper footer size and collectionViewContentSize.
Like, for the first time when this screen is loaded, I get following data listed on left-side while when I do some action on that particular screen by tapping button and which in turn calls updateCollectionViewLayout() again to create new flow layout and execute resize footer from viewDidLayoutSubviews(), I get following data listed on right-side.
size = 656 VS size = 656
layoutViewSize = (375.0, 414.0) | layoutViewSize = (375.0, 576.33)
- width : 375.0 | - width : 375.0
- height : 414.0 | - height : 576.33
|
footerView = (0 394; 375 20); | footerView = (0 499.333; 375 77);
- height : 20 | - height : 77
|
So, footerSize = {1, 262} | So, footerSize = {1, 156.66}
- height : 262 | - height : 156.66
Here, you will be able to see difference in layoutViewSize and footerView both which in turn affects footerSize.
Would anyone suggest me better option where to call resizeFooter method, so, I get proper contentViewSize once data is loaded? But I don't want that kind of jumpy solution where footer jumps to its original size post loading of all data.
ViewDidLoad() method just has call to fetchData API and nothing else, so, anything in viewDidLoad does not affect the collectionview.
I want it to be seamless so UX can not be affected.
Please provide me any suggestion.

Related

How can I detect orthogonal scroll events when using `UICollectionViewCompositionalLayout`?

In the video Advances in Collection View Layout - WWDC 2019, Apple introduces a new 'orthogonal scrolling behavior' feature. I have a view controller almost identical to OrthogonalScrollingViewController in their example code. In particular my collection view is laid out vertically, and each section can scroll horizontally (I use section.orthogonalScrollingBehavior = .groupPaging).
I want to have all my sections scroll horizontally in unison. Previously, I listened for scrollViewDidScroll on each horizontal collection view, then manually set the content offset of the others. However, with the new orthogonalScrollingBehavior implementation, scrollViewDidScroll never gets called on the delegate when I scroll horizontally. How can I detect horizontal scrolling events with the new API?
If there's another way to make the sections scroll together horizontally, I'm also open to other suggestions.
You can use this callback:
let section = NSCollectionLayoutSection(group: group)
section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in
}
As mentioned you can use visibleItemsInvalidationHandler which provides the location of the scroll offset.
You can detect if a page changed by getting the modulus of the page width. You need to additionally supply a tolerance to ignore halfway scroll changes.
Im using this:
class CollectionView: UICollectionViewController {
private var currentPage: Int = 0 {
didSet {
if oldValue != currentPage {
print("The page changed to \(currentPage)")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Configure layout...
let itemSize = NSCollectionLayoutSize...
let item = NSCollectionLayoutItem...
let groupSize = NSCollectionLayoutSize...
let group = NSCollectionLayoutGroup.horizontal...
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
// Use visibleItemsInvalidationHandler to make calculations
section.visibleItemsInvalidationHandler = { [weak self] items, location, environment in
guard let self = self else { return }
let width = self.collectionView.bounds.width
let scrollOffset = location.x
let modulo = scrollOffset.truncatingRemainder(dividingBy: width)
let tolerance = width/5
if modulo < tolerance {
self.currentPage = Int(scrollOffset/width)
}
}
self.collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(section: section)
}
}
Here's a hacky solution. Once you render your orthogonal section, you can access it via the subviews on your collectionView. You can then check if the subview is subclass of UIScrollView and replace the delegate.
collectionView.subviews.forEach { (subview) in
if let v = subview as? UIScrollView {
customDelegate.originalDelegate = v.delegate!
v.delegate = customDelegate
}
}
One tricky bit is that you want to capture its original delegate. The reason for this is because I notice that you must call originalDelegate.scrollViewDidScroll(scrollView) otherwise the section doesn't render out completely.
In other word something like:
class CustomDelegate: NSObject, UIScrollViewDelegate {
var originalDelegate: UIScrollViewDelegate!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
originalDelegate.scrollViewDidScroll?(scrollView)
}
}
You can do this:
section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
let indexPath = visibleItems.last!.indexPath
self?.pageControl.currentPage = indexPath.row
}
The collectionView delegate willDisplay method will tell you when a cell is added to the collectionView (e.g. is displayed on screen, as they are removed when they go offscreen).
That should let you know that panning has effectively occurred (and in most cases the important part is not the pan gesture or animation but how it affects the displayed content).
In that delegate method, collectionView.visibleCells can be used to determine what cells are displayed and from that one can derive the position.
I have found one convenient way to handle this issue, you can avoid setting orthogonal scrolling and use configuration instead this way:
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(sectionProvider:sectionProvider,configuration: config)
This will call all scroll delegates for collectionview. Hope this will be helpful for someone.

iOS - Custom view: Intrinsic content size ignored after updated in layoutSubviews()

I have a tableview cell containing a custom view among other views and autolayout is used.
The purpose of my custom view is to layout its subviews in rows and breaks into a new row if the current subview does not fit in the current line. It kind of works like a multiline label but with views. I achieved this through exact positioning instead of autolayout.
Since I only know the width of my view in layoutSubviews(), I need to calculate the exact positions and number of lines there. This worked out well, but the frame(zero) of my view didn't match because of missing intrinsicContentSize.
So I added a check to the end of my calculation if my height changed since the last layout pass. If it did I update the height property which is used in my intrinsicContentSize property and call invalidateIntrinsicContentSize().
I observed that initially layoutSubviews() is called twice. The first pass works well and the intrinsicContentSize is taken into account even though the width of the cell is smaller than it should be. The second pass uses the actual width and also updates the intrinsicContentSize. However the parent(contentView in tableview cell) ignores this new intrinsicContentSize.
So basically the result is that the subviews are layout and drawn correctly but the frame of the custom view is not updated/used in parent.
The question:
Is there a way to notify the parent about the change of the intrinsic size or a designated place to update the size calculated in layoutSubviews() so the new size is used in the parent?
Edit:
Here is the code in my custom view.
FYI: 8 is just the vertical and horizontal space between two subviews
class WrapView : UIView {
var height = CGFloat.zero
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: height)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size.width != .zero else { return }
// Make subviews calc their size
subviews.forEach { $0.sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ $0.frame.size.width }).max() ?? .zero <= frame.size.width else { return }
let width = frame.size.width
var row = [UIView]()
// rem is the remaining space in the current row
var rem = width
var y: CGFloat = .zero
var i = 0
while i < subviews.count {
let view = subviews[i]
let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
let last = i == subviews.count - 1
let fit = rem >= sizeNeeded
if fit {
row.append(view)
rem -= sizeNeeded
i += 1
guard last else { continue }
}
let rowWidth = row.map { $0.frame.size.width + 8 }.reduce(-8, +)
var x = (width - rowWidth) * 0.5
for vw in row {
vw.frame.origin = CGPoint(x: x, y: y)
x += vw.frame.width + 8
}
y += row.map { $0.frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}
After a lot of trying and research I finally solved the bug.
As #DonMag mentioned in the comments the new size of the cell wasn't recognized until a new layout pass. This could be verified by scrolling the cell off-screen and back in which showed the correct layout. Unfortunately it is harder than expected to trigger new pass as .beginUpdates() + .endUpdates()didn't
do the job.
Anyway I didn't find a way to trigger it but I followed the instructions described in this answer. Especially the part with the prototype cell for the height calculation provided a value which can be returned in tableview(heightForRowAt:).
Swift 5:
This is the code used for calculation:
let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1
The value is only calculated once and the cached as the size doesn't change anymore in my case.
Then the value can be returned in the delegate:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? height : UITableView.automaticDimension
}
I only used for the first cell as it is my header cell and there is only one section.

UI CollectionView in UICollectionView Cell Programmatically

Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.

Populating UICollectionView in reverse order

I would like to populate UICollectionView in reverse order so that the last item of the UICollectionView fills first and then the second last and so on. Actually I'm applying animation and items are showing up one by one. Therefore, I want the last item to show up first.
Swift 4.2
I found a simple solution and worked for me to show last item first of a collection view:
Inside viewDidLoad() method:
collectionView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
and inside collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) method before returning the cell:
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
(optional) Below lines will be necessary to auto scroll and show new item with smooth scroll.
Add below lines after loading new data:
if self.dataCollection.count > 0 {
self.collectionView.scrollToItem(at: //scroll collection view to indexpath
NSIndexPath.init(row:(self.collectionView?.numberOfItems(inSection: 0))!-1, //get last item of self collectionview (number of items -1)
section: 0) as IndexPath //scroll to bottom of current section
, at: UICollectionView.ScrollPosition.bottom, //right, left, top, bottom, centeredHorizontally, centeredVertically
animated: true)
}
I'm surprised that Apple scares people away from writing their own UICollectionViewLayout in the documentation. It's really very straightforward. Here's an implementation that I just used in an app that will do exactly what are asking. New items appear at the bottom, and the while there is not enough content to fill up the screen the the items are bottom justified, like you see in message apps. In other words item zero in your data source is the lowest item in the stack.
This code assumes that you have multiple sections, each with items of a fixed height and no spaces between items, and the full width of the collection view. If your layout is more complicated, such as different spacing between sections and items, or variable height items, Apple's intention is that you use the prepare() callback to do the heavy lifting and cache size information for later use.
This code uses Swift 3.0.
//
// Created by John Lyon-Smith on 1/7/17.
// Copyright © 2017 John Lyon-Smith. All rights reserved.
//
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100.00 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}
Just click on UICollectionView in storyboard,
in inspector menu under view section change semantic to Force Right-to-Left
I have attach an image to show how to do it in the inspector menu:
I'm assuming you are using UICollectionViewFlawLayout, and this doesn't have logic to do that, it only works in a TOP-LEFT BOTTOM-RIGHT order. To do that you have to build your own layout, which you can do creating a new object that inherits from UICollectionViewLayout.
It seems like a lot of work but is not really that much, you have to implement 4 methods, and since your layout is just bottom-up should be easy to know the frames of each cell.
Check the apple tutorial here: https://developer.apple.com/library/prerelease/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
The data collection does not actually have to be modified but that will produce the expected result. Since you control the following method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Simply return cells created from inverting the requested index. The index path is the cell's index in the collection, not necessarily the index in the source data set. I used this for a reversed display from a CoreData set.
let desiredIndex = dataProfile!.itemEntries!.count - indexPath[1] - 1;
Don't know if this still would be useful but I guess it might be quite useful for others.
If your collection view's cells are of the same height there is actually a much less complicated solution for your problem than building a custom UICollectionViewLayout.
Firstly, just make an outlet of your collection view's top constraint and add this code to the view controller:
-(void)viewWillAppear:(BOOL)animated {
[self.view layoutIfNeeded]; //for letting the compiler know the actual height and width of your collection view before we start to operate with it
if (self.collectionView.frame.size.height > self.collectionView.collectionViewLayout.collectionViewContentSize.height) {
self.collectionViewTopConstraint.constant = self.collectionView.frame.size.height - self.collectionView.collectionViewLayout.collectionViewContentSize.height;
}
So basically you calculate the difference between collection view's height and its content only if the view's height is bigger. Then you adjust it to the constraint's constant. Pretty simple. But if you need to implement cell resizing as well, this code won't be enough. But I guess this approach may be quite useful. Hope this helps.
A simple working solution is here!
// Change the collection view layer transform.
collectionView.transform3D = CATransform3DMakeScale(1, -1, 1)
// Change the cell layer transform.
cell.transform3D = CATransform3DMakeScale(1, -1, 1)
It is as simple as:
yourCollectionView.inverted = true
PS : Same for Texture/IGListKit..

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