UICollectionview dynamic height issue - ios

I want to achieve the following outcome
I have implemented collectionview horizontal scrolling. It is working fine when scrolling is enabled. However, when scrolling is disabled and the flow changes to vertical, only the first row is displayed and collection view height is not increased.
Q:- How can I get collection view full height when scrolling is disabled?
I have tried the following code for getting height and it returns 30.0.
let height = self.collectionTags.collectionViewLayout.collectionViewContentSize.height; // 30.0
And this one returns 0.0
let height = self.collectionTags.contentSize.height // 0.0
Here is the collectionview layout code
if let flowLayout = collectionTags.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1, height: 30.0)
flowLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize
}

I ran into a similar problem. The solution for me was to add height constraint to the collectionView and calculating its height depending on the items that I wanted to display. Here is a snippet I created:
// Items that will be displayed inside CollectionView are called tags here
// Total tags width
var tagWidth:CGFloat = 0
// Loop through array of tags
for tag in self.tagsArray {
// Specifying font so we can calculate dimensions
let tagFont = UIFont(name: "Cabin-Bold", size: 17)!
let tagAttributes: [String : Any]? = [
NSFontAttributeName: tagFont,
NSForegroundColorAttributeName: UIColor.white,
]
// Get text size
let tagString = tag as NSString
let textSize = tagString.size(attributes: tagAttributes ?? nil)
// Add current tag width + padding to the total width
tagWidth += (textSize.width + 10)
}
// Calculate number of rows
var totalRows = tagWidth / (self.bounds.width - 40)
totalRows = totalRows.rounded(.up)
// Calculate height and apply it to the CollectionView height constraint
estimatedCollectionViewHeight = Float(CGFloat(totalRows * 29))
collectionViewHeight.constant = CGFloat(estimatedCollectionViewHeight)

Related

How to change view size according to its content in iOS swift

I am working on photo editing app. I want to put some label and images in a view which is named as 'stickerView'. All I want when content size in that view change the stickerview also change its height and width accordingly.
For sticker view I am using https://github.com/injap2017/StickerView this library.
its working fine with images but with label its not adjust label font according to view.
I use this class to set the font according to the view
import UIKit
class FlexiFontLabel: UILabel {
// Boundary of minimum and maximum
private let maxFontSize = CGFloat(100)
private let minFontSize = CGFloat(15)
// Margin of error is needed in binary search
// so we can return when reach close enough
private let marginOFError = CGFloat(0.5)
// layoutSubviews() will get called while updating
// font size so we want to lock adjustments if
// processing is already in progress
private var isUpdatingFontSize = false
// Once this is set to true, the label should only
// only support multiple lines rather than one
var doesAdjustFontSizeToFitFrame = false
{
didSet
{
if doesAdjustFontSizeToFitFrame
{
numberOfLines = 0
}
}
}
// Adjusting the frame of the label automatically calls this
override func layoutSubviews()
{
super.layoutSubviews()
// Make sure the label is set to auto adjust the font
// and it is not currently processing the font size
if doesAdjustFontSizeToFitFrame
&& !isUpdatingFontSize
{
adjustFontSizeIfRequired()
}
}
/// Adjusts the font size to fit the label's frame using binary search
private func adjustFontSizeIfRequired()
{
guard let currentText = text,
var currentFont = font else
{
print("failed")
return
}
// Lock function from being called from layout subviews
isUpdatingFontSize = true
// Set max and min font sizes
var currentMaxFontSize = maxFontSize
var currentMinFontSize = minFontSize
while true
{
// Binary search between min and max
let midFontSize = (currentMaxFontSize + currentMinFontSize) / 2;
// Exit if approached minFontSize enough
if (midFontSize - currentMinFontSize <= marginOFError)
{
// Set min font size and exit because we reached
// the biggest font size that fits
currentFont = UIFont(name: currentFont.fontName,
size: currentMinFontSize)!
break;
}
else
{
// Set the current font size to the midpoint
currentFont = UIFont(name: currentFont.fontName,
size: midFontSize)!
}
// Configure an attributed string which can be used to find an
// appropriate rectangle for a font size using its boundingRect
// function
let attribute = [NSAttributedString.Key.font: currentFont]
let attributedString = NSAttributedString(string: currentText,
attributes: attribute)
let options: NSStringDrawingOptions = [.usesLineFragmentOrigin,
.usesFontLeading]
// Get a bounding box with the width of the current label and
// an unlimited height
let constrainedSize = CGSize(width: frame.width,
height: CGFloat.greatestFiniteMagnitude)
// Get the appropriate rectangle for the text using the current
// midpoint font
let newRect = attributedString.boundingRect(with: constrainedSize,
options: options,
context: nil)
// Get the current area of the new rect and the current
// label's bounds
let newArea = newRect.width * newRect.height
let currentArea = bounds.width * bounds.height
// See if the new frame is lesser than the current label's area
if newArea < currentArea
{
// The best font size is in the bigger half
currentMinFontSize = midFontSize + 1
}
else
{
// The best font size is in the smaller half
currentMaxFontSize = midFontSize - 1
}
}
// set the font of the current label
font = currentFont
// Open label to be adjusted again
isUpdatingFontSize = false
}
}
here I set sticker view and label:
var testLabel = FlexiFontLabel(frame: CGRect.init(x: 0, y: 0, width: 300, height: 50))
testLabel.text = "Test Label"
testLabel.textAlignment = .left
testLabel.backgroundColor = .blue
testLabel.adjustsFontForContentSizeCategory = true
testLabel.numberOfLines = 1
testLabel.adjustsFontSizeToFitWidth = true
testLabel.minimumScaleFactor = 0.2
testLabel.font = testLabel.font.withSize(testLabel.frame.height * 2/3)
testLabel.doesAdjustFontSizeToFitFrame = true
let stickerView2 = StickerView.init(contentView: testLabel)
stickerView2.center = CGPoint.init(x: 100, y: 100)
stickerView2.delegate = self
stickerView2.setImage(UIImage.init(named: "Close")!, forHandler: StickerViewHandler.close)
stickerView2.setImage(UIImage.init(named: "Rotate")!, forHandler: StickerViewHandler.rotate)
stickerView2.showEditingHandlers = false
self.view.addSubview(stickerView2)
its some how working fine for me but when I change font size of label using slider its size does not change.
So my question is how to change font size and stickerview size accordingly.
if I change font size using slider label size and stickerview size change accordingly and if I change view width and height its change label font size accordingly.
help will be appreciated thanks.

How to snap horizontal paging to multi-row collection view like App Store?

I would like to replicate the paging in the multi-row App Store collection view:
So far I've designed it as close as possible to the way it looks, including showing a peek to the previous and next cells, but do not know how to make the paging to work so it snaps the next group of 3:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = MultiRowLayout(
rowsCount: 3,
inset: 16
)
}
...
class MultiRowLayout: UICollectionViewFlowLayout {
private var rowsCount: CGFloat = 0
convenience init(rowsCount: CGFloat, spacing: CGFloat? = nil, inset: CGFloat? = nil) {
self.init()
self.scrollDirection = .horizontal
self.minimumInteritemSpacing = 0
self.rowsCount = rowsCount
if let spacing = spacing {
self.minimumLineSpacing = spacing
}
if let inset = inset {
self.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
}
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
self.itemSize = calculateItemSize(from: collectionView.bounds.size)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView,
!newBounds.size.equalTo(collectionView.bounds.size) else {
return false
}
itemSize = calculateItemSize(from: collectionView.bounds.size)
return true
}
}
private extension MultiRowLayout {
func calculateItemSize(from bounds: CGSize) -> CGSize {
return CGSize(
width: bounds.width - minimumLineSpacing * 2 - sectionInset.left,
height: bounds.height / rowsCount
)
}
}
Unfortunately, the native isPagingEnabled flag on UICollectionView only works if the cell is 100% width of the collection view, so the user wouldn’t get a peek and the previous and next cell.
I have a working snap paging functionality but only for a single item per page, not this 3-row kind of collection. Can someone help make the snap paging work for the grouped rows instead of for a single item per page?
There is no reason to subclass UICollectionViewFlowLayout just for this behavior.
UICollectionView is a subclass of UIScrollView, so its delegate protocol UICollectionViewDelegate is a subtype of UIScrollViewDelegate. This means you can implement any of UIScrollViewDelegate’s methods in your collection view’s delegate.
In your collection view’s delegate, implement scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) to round the target content offset to the top left corner of the nearest column of cells.
Here's an example implementation:
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let bounds = scrollView.bounds
let xTarget = targetContentOffset.pointee.x
// This is the max contentOffset.x to allow. With this as contentOffset.x, the right edge of the last column of cells is at the right edge of the collection view's frame.
let xMax = scrollView.contentSize.width - scrollView.bounds.width
if abs(velocity.x) <= snapToMostVisibleColumnVelocityThreshold {
let xCenter = scrollView.bounds.midX
let poses = layout.layoutAttributesForElements(in: bounds) ?? []
// Find the column whose center is closest to the collection view's visible rect's center.
let x = poses.min(by: { abs($0.center.x - xCenter) < abs($1.center.x - xCenter) })?.frame.origin.x ?? 0
targetContentOffset.pointee.x = x
} else if velocity.x > 0 {
let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
// Find the leftmost column beyond the current position.
let xCurrent = scrollView.contentOffset.x
let x = poses.filter({ $0.frame.origin.x > xCurrent}).min(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? xMax
targetContentOffset.pointee.x = min(x, xMax)
} else {
let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget - bounds.size.width, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
// Find the rightmost column.
let x = poses.max(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? 0
targetContentOffset.pointee.x = max(x, 0)
}
}
// Velocity is measured in points per millisecond.
private var snapToMostVisibleColumnVelocityThreshold: CGFloat { return 0.3 }
Result:
You can find the full source code for my test project here: https://github.com/mayoff/multiRowSnapper
With iOS 13 this became a lot easier!
In iOS 13 you can use a UICollectionViewCompositionalLayout.
This introduces a couple of concepts and I will try to give a gist over here, but I think is worth a lot to understand this!
Concepts
In a CompositionalLayout you have 3 entities that allow you to specify sizes. You can specify sizes using absolute values, fractional values (half, for instance) or estimates. The 3 entities are:
Item (NSCollectionLayoutItem)
Your cell size. Fractional sizes are relative to the group the item is in and they can consider the width or the height of the parent.
Group (NSCollectionLayoutGroup)
Groups allow you to create a set of items. In that app store example, a group is a column and has 3 items, so your items should take 0.33 height from the group. Then, you can say that the group takes 300 height, for instance.
Section(NSCollectionLayoutSection)
Section declares how the group will repeat itself. In this case it is useful for you to say the section will be horizontal.
Creating the layout
You create your layout with a closure that receives a section index and a NSCollectionLayoutEnvironment. This is useful because you can have different layouts per trait (on iPad you can have something different, for instance) and per section index (i.e, you can have 1 section with horizontal scroll and another that just lays out things vertically).
func createCollectionViewLayout() {
let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
return self.createAppsColumnsLayout()
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .vertical
config.interSectionSpacing = 31
layout.configuration = config
return layout
}
The app store example
In the case of the app store you have a really good video by Paul Hudson, from the Hacking with Swift explaining this. He also has a repo with this!
However, I will put here the code so this doesn't get lost:
func createAppsColumnsLayout(using section: Section) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(0.33)
)
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 5,
bottom: 0,
trailing: 5
)
let layoutGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.93),
heightDimension: .fractionalWidth(0.55)
)
let layoutGroup = NSCollectionLayoutGroup.vertical(
layoutSize: layoutGroupSize,
subitems: [layoutItem]
)
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
return layoutSection
}
Finally, you just need to set your layout:
collectionView.collectionViewLayout = createCompositionalLayout()
One cool thing that came with UICollectionViewCompositionalLayout is different page mechanisms, such as groupPagingCentered, but I think this answer already is long enough to explain the difference between them 😅
A UICollectionView (.scrollDirection = .horizontal) can be used as an outer container for containing each list in its individual UICollectionViewCell.
Each list in turn cab be built using separate UICollectionView(.scrollDirection = .vertical).
Enable paging on the outer UICollectionView using collectionView.isPagingEnabled = true
Its a Boolean value that determines whether paging is enabled for the scroll view.
If the value of this property is true, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls. The default value is false.
Note: Reset left and right content insets to remove the extra spacing on the sides of each page.
e.g. collectionView?.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)

Calculating height of UICollectionViewCell with text only

trying to calculate height of a cell with specified width and cannot make it right. Here is a snippet. There are two columns specified by the custom layout which knows the column width.
let cell = TextNoteCell2.loadFromNib()
var frame = cell.frame
frame.size.width = columnWidth // 187.5
frame.size.height = 0 // it does not work either without this line.
cell.frame = frame
cell.update(text: note.text)
cell.contentView.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(CGSize(width: columnWidth, height: 0)) // 251.5 x 52.5
print(cell) // 187.5 x 0
return size.height
Both size and cell.frame are incorrect.
Cell has a text label inside with 16px margins on each label edge.
Thank you in advance.
To calculate the size for a UILabel to fully display the given text, i would add a helper as below,
extension UILabel {
public static func estimatedSize(_ text: String, targetSize: CGSize = .zero) -> CGSize {
let label = UILabel(frame: .zero)
label.numberOfLines = 0
label.text = text
return label.sizeThatFits(targetSize)
}
}
Now that you know how much size is required for your text, you can calculate the cell size by adding the margins you specified in the cell i.e 16.0 on each side so, the calculation should be as below,
let intrinsicMargin: CGFloat = 16.0 + 16.0
let targetWidth: CGFloat = 187.0 - intrinsicMargin
let labelSize = UILabel.estimatedSize(note.text, targetSize: CGSize(width: targetWidth, height: 0))
let cellSize = CGSize(width: labelSize.width + intrinsicMargin, height: labelSize.height + intrinsicMargin)
Hope you will get the required results. One more improvement would be to calculate the width based on the screen size and number of columns instead of hard coded 187.0
That cell you are loading from a nib has no view to be placed in, so it has an incorrect frame.
You need to either manually add it to a view, then measure it, or you'll need to dequeu it from the collectionView so it's already within a container view
For Swift 4.2 updated answer is to handle height and width of uicollectionview Cell on the basis of uilabel text
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let size = (self.FILTERTitles[indexPath.row] as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0)])
return CGSize(width: size.width + 38.0, height: size.height + 25.0)
}

Updating UICollectionViewFlowLayout when the height of my CollectionView changes during runtime

I have a MyCollectionView that has a constraint that is:
BottomLayoutGuide.Top = MyCollectionView.Bottom + 100
In my IB, but this constraint's constant changes according to the height of the keyboard
func keyboardWillShow(notification: NSNotification) {
let keyboardHeight = getKeyboardHeight(notification)
keyboardHeightConstraint.constant = keyboardHeight + 20
self.cardCollectionView.configureCollectionView()
self.cardCollectionView.reloadInputViews()
}
Where configureCollectionView is:
func configureCollectionView() {
self.backgroundColor = UIColor.clearColor()
// Create the layout
let space = 10.0 as CGFloat
let flowLayout = UICollectionViewFlowLayout()
let width = (self.frame.size.width) - 4 * space
let height = (self.frame.size.height)
let edgeInsets = UIEdgeInsetsMake(0, 2 * space, 0, 2 * space)
// Set top and bottom margins if scrolling horizontally, left and right margins if scrolling vertically
flowLayout.minimumLineSpacing = space
flowLayout.minimumInteritemSpacing = 0
// Set horizontal scrolling
flowLayout.scrollDirection = .Horizontal
// Set edge insets
flowLayout.sectionInset = edgeInsets
flowLayout.itemSize = CGSizeMake(width, height)
print(self.frame)
print(flowLayout.itemSize)
self.setCollectionViewLayout(flowLayout, animated: true)
// Always allow it to scroll
self.alwaysBounceHorizontal = true
}
Before the keyboard is shown, everything is fine and these are the values of the frame of MyCollectionView and it's CollectionViewCell
self.frame: (0.0, 75.0, 375.0, 492.0)
flowLayout.itemSize: (335.0, 492.0)
But after the keyboard is shown and configureCollectionView() is called which basically just resets the flowLayout for MyCollectionView everything breaks. And oddly, even though the height of MyCollectionView has decreased the console outputs says that it has actually increased.
self.frame: (0.0, 55.0, 375.0, 512.0)
flowLayout.itemSize: (335.0, 512.0)
Here is a screenshot after the keyboard has appeared:
As you can see, it seems like the CollectionViewCells are being clipped and it has lost it's initial alpha value. Moreover, after randomly scrolling the console outputs:
2016-04-29 09:43:24.012 DeckWheel[15363:2028073] the behavior of the UICollectionViewFlowLayout is not defined because:
2016-04-29 09:43:24.012 DeckWheel[15363:2028073] 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.
I know that this is due to the height of MyCollectionView changing but, I don't know why it is going wrong as everything works perfectly before the keyboard shows. For example, I can scroll randomly between the cells and if I get to the last cell it will automatically create a new one without any crashes.
I've tried MyCollectionView.invalidateLayout() and then callingconfigureCollectionView() to reset the flowLayout but nothing seems to work. What is the correct way to update MyCollectionView so that this does not happen?

UICollectionView layout change when frame size changes

I'm working with a UICollectionView but I have a weird behaviour.
I want a horizontal scroll, small cell height ( smaller than the UICollectionView height).
Tapping a button will increase even more the height of the collection.
The problem is that the layout of my cells is changing also.
I want the collection view height to increase with the cells staying in the same position.
Here attached 2 screen captures (before/after button touched).
Here is my code:
var smallLayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout();
smallLayout.scrollDirection = .Horizontal
var itemHeight = 100
smallLayout.itemSize = CGSizeMake(150, itemHeight);
var bottomDist:CGFloat = 1
var topDist = collHeight - itemHeight - bottomDist
smallLayout.sectionInset = UIEdgeInsets(top: topDist, left: 2, bottom: bottomDist, right: 2)
smallLayout.minimumInteritemSpacing = 15
smallLayout.minimumLineSpacing = 10
So, when touching the button I have the following code:
let viewHeight = CGRectGetHeight(self.view.frame)
let viewWidth = CGRectGetWidth(self.view.frame)
var collHeight:CGFloat = 600
var collYPosition = viewHeight-CGFloat(collHeight)-20
var rect = CGRectMake(0, CGFloat(collYPosition), CGFloat(viewWidth), CGFloat(collHeight))
dishCollectionView.frame = rect
So, I cannot understand why my layout is changing.
I want to have my cell at the bottom.
Perhaps if you reset the top of your sectionInset to accommodate for the additional height, i.e. an updated topDist calculation. Try adding something like this to you on touch code:
var bottomDist:CGFloat = 1
var topDist = collHeight - itemHeight - bottomDist
dishCollectionView.collectionViewLayout.sectionInset = UIEdgeInsets(top: topDist, left: 2, bottom: bottomDist, right: 2)
dishCollectionView.collectionViewLayout.invalidateLayout()

Resources