UICollectionView container padding - ios

I am trying to add padding to the container of a UICollectionView. I would like it to appear as such that there is a 10pt padding all around. So in the example screen, there is a 10pt padding on the bottom from:
layout.minimumInteritemSpacing = layout.minimumLineSpacing = 10;
I am using a UICollectionViewFlowLayout to layout the cells. I have also tried a "trick" where I add a 10pt view on top, but the content doesn't appear to scroll through the view since they are separate.

Please try to put following code inside viewDidLoad():
collectionView!.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
This will add padding to every side of the collectionView.
PS: I know the question is here for a while but it might help somebody ;)

It seems like the sectionInset property of your UICollectionViewFlowLayout is what you need to modify.
You can use a UIEdgeInsetsMake to create a UIEdgeInsets with margins for top, left, right, and bottom, and set this to the sectionInset property.
Here's the documentation for UICollectionViewFlowLayout: https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewFlowLayout_class/Reference/Reference.html

I used a couple of methods from the UICollectionViewDelegateFlowLayout delegate to accomplish what I was looking for. I was attempting to add 'padding' to cells that had a shadow. This was to ensure they had the effect of 'floating,' and were not touching the edges of the UICollectionView.
First, call the sizeForItemAt indexPath method and give the cells a size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width * 0.80, height: collectionView.bounds.height * 0.98)
}
Then, call the insetForSectionAt section method, and set the insets for the cells:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 25, bottom: 20, right: 25)
}

If you're using configurations, set the contentInsetsReference on the layout's configuration to follow layoutMargins, then set layoutMargins on the collectionView itself.
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: config)
layout.configuration.contentInsetsReference = .layoutMargins
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.layoutMargins = .init(top: 16, left: 64, bottom: 16, right: 64)

In case you're using a UICollectionLayoutListConfiguration, it seems that to get smaller margins with certain appearances, you've to do that on section level like this:
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
// Create a list configuration as usual
var config = UICollectionLayoutListConfiguration(appearance: .sidebarPlain)
config.headerMode = .supplementary
// Change the section's contentInsets to adjust the side margins
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
section.contentInsets.leading = 10
section.contentInsets.trailing = 10
return section
}
return layout
}
Based on feedback from Apple (https://developer.apple.com/forums/thread/679863)

Related

UICollectionViewCell sizeForItemAt values being reverted to fitItemSize somehow?

I have set up 2 collectionView in a viewcontroller, both get their data from an endpoint and then reloadData().
One collectionView act like an header tab and have its cell size depend on its intritic size and rely on insetForSection to position/align the cell in the center of the collectionView.
Another have "sort-of" fixed size for themselves where the first cell will be almost the entire width of the collectionView and then the cells after the first one will occupy semi-half the collectionView width.
I have setted-up delegate and extension methods, however for some reason the sizeforItem that focus on the second CollectionView doesn't "stick", they get reverted. When i do :
self.statusOptionCollectionView.reloadData()
self.statusOptionCollectionView.performBatchUpdates({
self.statusOptionCollectionView.layoutIfNeeded()
}) { (complete) in
debugPrint("Batch Update complete")
}
I saw a brief frame of my desired outcome but then the collectionView suddenly undo my sizeForItem code and change the cell to something akin to "size-to-fit". (Pics: Below).
Question is how do i fix this? What is causing this? Is it because i have 2 collectionView in one viewcontroller? I've tried to invalidatingLayout in viewdidlayoutsubviews but it doesn't work. "I did use storyboard but i already delete the collectionView and re-add it, didn't fix it)
I want Something Like This (Focus one the second viewcontroller layout) :
My CollectionView Layout Code is Like This (kindTabCollectionView is the "header", with center alignment) :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if collectionView.isEqual(self.kindTabCollectionView){
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let totalCellWidth = layout.itemSize.width * CGFloat(self.kindArray.count)
let totalSpacingWidth = CGFloat(8 * (self.kindArray.count - 1))
let leftInset = (collectionView.bounds.size.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
let rightInset = leftInset
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}else{
return UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if collectionView.isEqual(self.kindTabCollectionView){
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return flowLayout.itemSize
}else{
let height = CGFloat(40.0)
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let widthMargin = (flowLayout.sectionInset.left + flowLayout.sectionInset.left + flowLayout.minimumInteritemSpacing)
if indexPath.item == 0 && indexPath.section == 0{
let width = floor(collectionView.frame.size.width - widthMargin)
return CGSize(width: width, height: height)
}else{
let width = floor((collectionView.frame.size.width / 2) - widthMargin)
return CGSize(width: width, height: height)
}
}
}
However, the result that come out is this :
(Sorry, it was just a few frame, i tried my best to screen shot it, but it did tried to change to correct frame size, but then it just revert to the small "fitToSize" pic)
Check collectionView's "Estimated Size" attribute in the Size Inspector (Storyboard). It should be set to "None" when using an extension of UICollectionViewDelegateFlowLayout to set cell's size.
As stated in the Xcode 11 Release Notes:
Cells in a UICollectionView can now self size with Auto Layout
constrained views in the canvas. To opt into the behavior for existing
collection views, enable “Automatic” for the collection view’s
estimated size, and “Automatic” for cell’s size from the Size
inspector. If deploying before iOS 13, you can activate self sizing
collection view cells by calling performBatchUpdates(_:completion:)
during viewDidLoad(). (45617083)
So, newly created collectionViews have the attribute "Estimated Size" set as "Automatic" and the cell's size is computed considering its subview dimensions, thus ignoring the UICollectionViewDelegateFlowLayout extension methods, even though they are called.

How to give a collection view section header dynamic height

I have a collection view with a section header that displays a users information. The header can include a bio from the user. I am having trouble with resizing the header if the bio is long. The label will not display the whole bio because the header stays at a fixed height.
I created the collection view via storyboard however I added the constraints programmatically.
Here is the bio constraints, I thought by setting the height and lines to 0 I would be okay,
addSubview(bioLabel)
bioLabel.anchor(top: editProfileButton.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 12, paddingLeft: 12, paddingBottom: 0, paddingRight: 12, width: 0, height: 0)
I also thought I could override the storyboard by
// Trying to override storyboard header height so the header can strech depending on the bio height
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
//TODO change height to be bounds.height of bioLabel?
return .init(width: view.frame.width, height: 300)
}
but it appears even if I try to change the size through code, it keeps the storyboard height of 225 ( which is what I would like)
None of the answers given so far are good practices since calculating the size manually or by rendering a label offscreen are both inefficient and violate the single source of truth principle.
Instances of UICollectionReusableView have the preferredLayoutAttributesFitting() method. When the cell becomes displayed, this method will be called. In this method, if you use Auto Layout, you ask the whole cell for its systemLayoutSizeFitting() and then modify the attributes. The layout will then be responsible to apply them via layout invalidation.
Use an instance of UICollectionReusableView to be your header.
And calculate the actual size of header in this method
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { }
Make sure you header is ready for dynamic height as well. Good luck.
You can get Label Height using below function
func heightForLabel(text: String, font: UIFont, width: CGFloat) -> CGFloat {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
Collection Header Method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let height = self.heightForLabel(text: 'your text', font: 'font which you in label' , width: 'your label width')
return .init(width: view.frame.width, height: height+10) //10 is extra height if you want or you can remove it.
}
This function will do the work. Just pass the label text
func estimatedLabelHeight(text: String) -> CGFloat {
let size = CGSize(width: view.frame.width - 16, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 10)]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
And use it like
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
//TODO change height to be bounds.height of bioLabel?
return .init(width: view.frame.width, height: estimatedLabelHeight(text: "Text that your label is gonna show"))
}
You need to calculate the size of cell height in method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
var size = CGSize()
let cell = YourUICollectionViewCell()
cell.textLabel?.text = data?[indexPath.row].text
let fitting = CGSize(width: cell.frame.size.width, height: 1)
size = cell.contentView.systemLayoutSizeFitting(fitting,
withHorizontalFittingPriority: .required,
verticalFittingPriority: UILayoutPriority(1))
return size.height //or just return size
}
If UILabel is custom then you need to set the bottom Constraint of UILabel to view with less than 1000 priority.
Hope it helps

how to use center vertically cells in collection view in paging mode in swift 4?

I read
How to vertically center UICollectionView content
But when I used the codes here
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let navBarHeight = self.navigationController?.navigationBar.frame.height
let collectionViewHeight = (self.collectionView?.frame.height)! - navBarHeight!
let itemsHeight = self.collectionView?.contentSize.height
let topInset = ( collectionViewHeight - itemsHeight! ) / 4
return UIEdgeInsetsMake(topInset ,0, 0 , 0)
}
But when scrolling collection view this will show incorrect form
so here is my codes because my collection view cells are square and equal to screen width
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
showImagesCV.contentInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
if let flowLayout = showImagesCV.collectionViewLayout as? UICollectionViewFlowLayout{
cellWidth = UIScreen.main.bounds.width
cellHeight = cellWidth //yourCellHeight = cellWidth if u want square cell
flowLayout.minimumLineSpacing = spacing
flowLayout.minimumInteritemSpacing = spacing
UIView.performWithoutAnimation {
}
}
}
}
extension showImagesViewController : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: cellWidth, height: cellHeight)
}
}
I want the spacing between cells be 0 and each cell that is showing (current page (because the collection view is in paging mode)) be the center of the screen
Regarding the SO Question you mentioned and using, that is to centre the whole section not individual cells. Some of your code might be invalid(needs to be redone).
First and most important, set the constraints of your collectionView for full screen. Remember to take safeAre/TopGuide into account, this will make sure that collection view is below the navigation bar if there is one.
This will make sure that your collectionView is up to date
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
Dump/comment below code and set insets to (0,0,0,0) in inspector of collectionView in story board. In same inspector change Min Spacing to 0 for cells and for line, both.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let navBarHeight = self.navigationController?.navigationBar.frame.height
let collectionViewHeight = (self.collectionView?.frame.height)! - navBarHeight!
let itemsHeight = self.collectionView?.contentSize.height
let topInset = ( collectionViewHeight - itemsHeight! ) / 4
return UIEdgeInsetsMake(topInset ,0, 0 , 0)
}
Also remove/comment below code
showImagesCV.contentInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
if let flowLayout = showImagesCV.collectionViewLayout as? UICollectionViewFlowLayout{
cellWidth = UIScreen.main.bounds.width
cellHeight = cellWidth //yourCellHeight = cellWidth if u want square cell
flowLayout.minimumLineSpacing = spacing
flowLayout.minimumInteritemSpacing = spacing
UIView.performWithoutAnimation {
}
}
Now change your sizeForItemAt like this.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.frame.size
}
UICollectionViewCell
Now in your collectionView cell, make sure that your image view is 1:1 and in centre. Also handle constraints of your other UI components.
PS : This is easy approach, other would be to write custom flowLayout

How to auto resize contents in CollectionViewCell after the cell's size changed?

In Xcode, I created a UICollectionView and dragged some labels to the UICollectionViewCell. The issue was that when changing devices, the cell won't adjust its size to the screen size. So I implemented the following code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
return CGSize(width: self.collectionView.frame.width * 0.9, height:self.collectionView.frame.height *0.8)
}
After this, I noticed that the cell's size did change. However, the width and height of the labels inside the cell remained the same.
I'm not sure how to resolve this problem.
Thank you in advance for your help!!
set Constraint of your labels,
like,
for Constraint pic.
your label pic.
or you can also use UI property autoresizingMask
like,
yourLbl.autoresizingMask = [.flexibleWidth, .flexibleHeight]

Setting minimumLineSpacing for UICollectionViewFlowLayout has no effect, must use insetForSectionAtIndex function?

The goal is to define the spacing between sections for a UICollectionView.
However, setting the UICollectionViewFlowLayout as follows with the minimumLineSpacing property has no effect:
// Init <collectionView>
collectionView.alwaysBounceVertical = true
collectionView.contentInset = UIEdgeInsets(top: CollectionViewTopPadding, left: 0, bottom: CollectionViewBottomPadding, right: 0)
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.minimumLineSpacing = 5
The only approach that's working is to use the insetForSectionAtIndex as follows:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)
}
Other examples on SO set minimumLineSpacing so can anyone explain why this code isn't working?
According to Apple documentation
The flow layout uses the value in minimumLineSpacing property to set the spacing between lines in a section.
Thus it will not set spacing between the sections, it is for setting space between the rows/columns within the section. If at all you want to achieve spacing between sections without implementing delegate method, you could use sectionInset property of UICollectionViewFlowLayout.
Incase someone is wondering how to only do it for one section:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if section == 0{
return UIEdgeInsetsMake(0, 0, 10, 0)
}
return UIEdgeInsetsMake(0, 0, 0, 0)
}

Resources