Swift give uicollectionviewcell width and dynamic height - ios

I have this uicollectionviewcell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = commentSection.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CommentCell
return cell
}
CommentCell:
let CommenterprofileImage: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let commentText: UILabel = {
let l = UILabel()
l.numberOfLines = 0
l.text = "some text"
l.translatesAutoresizingMaskIntoConstraints = false
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .blue
designCell()
}
func designCell(){
addSubview(CommenterprofileImage)
addSubview(commentText)
addCellConstraints()
}
func addCellConstraints(){
CommenterprofileImage.topAnchor.constraint(equalTo: self.topAnchor,constant:10).isActive = true
CommenterprofileImage.leftAnchor.constraint(equalTo: self.leftAnchor,constant:20).isActive = true
CommenterprofileImage.widthAnchor.constraint(equalToConstant: 70).isActive = true
CommenterprofileImage.heightAnchor.constraint(equalToConstant: 70).isActive = true
commentText.topAnchor.constraint(equalTo: CommenterprofileImage.bottomAnchor,constant:20).isActive = true
commentText.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
commentText.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
}
And i want this cell's width to be equal to the view's width,but i want it's height to depend on it's content.
I tried doing this
layout.estimatedItemSize = CGSize(width: view.frame.width, height: 100)
But then my cell's height is 100 and width is less than 100.

You Can do it this way-:
Calculate label height with boundingRect method first-:
func estimatedFrameHeight(text:String) -> CGRect{
let size = CGSize(width: (view.frame.width/2 - 8), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSFontAttributeName:UIFont.systemFont(ofSize: 11)]
return NSString(string: text).boundingRect(with: size, options: options, attributes: attribute, context: nil)
}
Keep the exact same font you providing to your label,And keep .usesLineFragmentOrigin for multi line label.
Now in CollectionView sizeForItemAt indexPath do this-:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.frame.width)
let subDict = subCategoryListArray[indexPath.row]
if let product_name = subDict["product_name"] as? String {
let height = estimatedFrameHeight(text:product_name)
return CGSize(width: width, height: height.height + 70 + 10 + 20)
}
return CGSize(width: 0, height: 0)
}
let height = estimatedFrameHeight(text:product_name) . This will provide you with estimated height as well as width if you need. Now you have your label height calculated you need to add imageView height , y position constraints to make it work.You can get it from here-:
CommenterprofileImage.heightAnchor.constraint(equalToConstant: 70).isActive = true
Height = 70 you have.
And,
CommenterprofileImage.topAnchor.constraint(equalTo: self.topAnchor,constant:10).isActive = true
commentText.topAnchor.constraint(equalTo: CommenterprofileImage.bottomAnchor,constant:20).isActive
Top Constraint is = 10
Top Constraint is = 20
So you need to add this now as you can see I did in answer.
Remark-:
only add ImageView Height if it is above label else not require, just
add height and y padding for what ever you have above label.
Same to calculate exact width for label subtarct leading or trailing
space.
If label covers complete width (No insets subtraction required)-:
func estimatedFrameHeight(text:String) -> CGRect{
let size = CGSize(width: (view.frame.width), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSFontAttributeName:UIFont.systemFont(ofSize: 11)]
return NSString(string: text).boundingRect(with: size, options: options, attributes: attribute, context: nil)
}
If label is 20 points from leading then subtract that-:
func estimatedFrameHeight(text:String) -> CGRect{
let size = CGSize(width: (view.frame.width - 20), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSFontAttributeName:UIFont.systemFont(ofSize: 11)]
return NSString(string: text).boundingRect(with: size, options: options, attributes: attribute, context: nil)
}

Related

UICollectionViewFlowLayout issues

Apologies in advance but I've been going around in circles on this for days...
I have a UIViewController that is presented from another VC (in my case, a button is tapped). The new VC (code below) is made up of:
a UITextView which dynamically increases in height based on content the user types in
a UIView which has a fixed height
and then below that, to the bottom of the ViewController view, is a UICollectionView. This has 5 sections in it that horizontally scroll. I will be inserting different content into each of those 5 cells but am just having issues at the moment when the UICollectionView resizes. I've been able to get most of them cleared except for one.
This error can be reproduced whenever the UITextView increases in size to between 3-6 lines long, and again at 13 lines... This happens in the simulator using e.g. an iPhone 8 plus but on an iPhone 11 Pro Max, it only happens around 19 lines of text in the UITextView.
I am using TinyContraints here but I've tested using traditional programmatic constraints and had the same issue.
//
// NewItemVC.swift
//
import TinyConstraints
import UIKit
class NewItemVC: UIViewController {
let titleField = UITextView()
let bannerContainer = UIView(frame: .zero)
var flowLayout = UICollectionViewFlowLayout()
lazy var horizontalCV = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
let genericCellID = "genericCellID"
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
setupHeaderArea()
setupBannerArea()
setupHorizontalCollectionView()
}
func setupHeaderArea(){
titleField.backgroundColor = .lightGray
titleField.textContainerInset.top = 55
titleField.textContainerInset.bottom = 20
titleField.textContainerInset.left = 15
titleField.textContainerInset.right = 15
titleField.font = UIFont.boldSystemFont(ofSize: 20)
// add to main view & position
view.addSubview(titleField)
titleField.topToSuperview()
titleField.leftToSuperview()
titleField.rightToSuperview()
titleField.isScrollEnabled = false
NotificationCenter.default.addObserver(self, selector: #selector(updateTextFieldHeight), name: UITextView.textDidChangeNotification, object: nil)
// add a header label
let headerLabel = UILabel()
headerLabel.textColor = .systemBlue
headerLabel.text = "NEW ITEM"
headerLabel.font = UIFont.systemFont(ofSize: 12)
view.addSubview(headerLabel)
headerLabel.topToSuperview(offset: 35)
headerLabel.leftToSuperview(offset: 20)
headerLabel.height(13)
// add card handle
let handle = UIView()
handle.backgroundColor = .black
handle.alpha = 0.2
handle.width(45)
handle.height(5)
handle.layer.cornerRadius = 5/2
view.addSubview(handle)
handle.topToSuperview(offset: 10)
handle.centerXToSuperview()
}
func setupBannerArea(){
// position container
view.addSubview(bannerContainer)
bannerContainer.topToBottom(of: titleField)
bannerContainer.edgesToSuperview(excluding: [.top, .bottom])
bannerContainer.height(62)
bannerContainer.backgroundColor = .cyan
}
func setupHorizontalCollectionView() {
horizontalCV = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
horizontalCV.dataSource = self
horizontalCV.delegate = self
horizontalCV.backgroundColor = .yellow
horizontalCV.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
horizontalCV.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
view.addSubview(horizontalCV)
horizontalCV.topToBottom(of: bannerContainer)
horizontalCV.edgesToSuperview(excluding: .top)
horizontalCV.isPagingEnabled = true
horizontalCV.contentInsetAdjustmentBehavior = .never
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 0
flowLayout.minimumInteritemSpacing = 0
horizontalCV.register(UICollectionViewCell.self, forCellWithReuseIdentifier: genericCellID)
}
#objc func updateTextFieldHeight() {
DispatchQueue.main.async{
self.horizontalCV.collectionViewLayout.invalidateLayout()
}
}
}
// MARK:- Delegates - UITextViewDelegate
extension NewItemVC: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: textView.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}
// MARK:- UICollectionView Data Source
extension NewItemVC: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: genericCellID, for: indexPath)
if indexPath.row % 2 == 0 {
cell.backgroundColor = .orange
} else {
cell.backgroundColor = .red
}
return cell
}
}
// MARK:- Delegates - UICollectionView Flow Layout
extension NewItemVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let requiredHeight = self.view.frame.height - (titleField.frame.height + bannerContainer.frame.height)
let requiredWidth = view.frame.width
return CGSize(width: requiredWidth, height: requiredHeight)
}
}
From what I've been able to tell, there's some issue randomly where the collectionView frame and the contentSize are out by 24 points but I can't work out why/where etc...
The error is as follows:
2020-02-06 20:39:18.425280+1100 Scrolling[67237:8005204] The behavior of the UICollectionViewFlowLayout is not defined because:
2020-02-06 20:39:18.425378+1100 Scrolling[67237:8005204] 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.
2020-02-06 20:39:18.425672+1100 Scrolling[67237:8005204] The relevant UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout: 0x7fcf56d4a120>, and it is attached to <UICollectionView: 0x7fcf5785d000; frame = (0 208.667; 414 487.333); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x600000ce8a50>; layer = <CALayer: 0x600000213a00>; contentOffset: {0, 0}; contentSize: {2070, 511}; adjustedContentInset: {0, 0, 0, 0}; layout: <UICollectionViewFlowLayout: 0x7fcf56d4a120>; dataSource: <Scrolling.NewItemVC: 0x7fcf56d20110>>.
2020-02-06 20:39:18.425760+1100 Scrolling[67237:8005204] Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.

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)
}

UITextView's boundingRect Not Working Properly

I'm currently have the following extension on UITextField to calculate the bounding rect for a given string.
func widthHeight(font: UIFont) -> CGRect {
let constraintRect = CGSize(width: 200, height: 1000)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox
}
The width for constraintRect is the maximum width I want to allow for the box.
I set the values and the cells like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuse, for: indexPath) as? ChatCollectionViewCell {
let text = self.chatLog[indexPath.row].text
cell.chatTextView.text = text
cell.chatViewWidth = (text?.widthHeight(font: UIFont.systemFont(ofSize: 16)).width)!
return cell
}
return UICollectionViewCell()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let text = self.chatLog[indexPath.row].text {
let box = text.widthHeight(font: UIFont.systemFont(ofSize: 16))
return CGSize(width: view.frame.width, height: box.height + 10)
}
return CGSize(width: self.view.frame.width, height: 60)
}
When this code runs, I get massively miscalculated cell sizes:
As you can see, the view's frames are very messed up.
The first line is "Heya", the second line is "How's life going so far", and the third line is "I'm a stapler, you're a textbook." Some cells are too narrow, some cells are too wide.
Here's some additional code for my custom collectionViewCell:
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
override func layoutSubviews() {
chatView.frame = CGRect(x: 0, y: 0, width: chatViewWidth, height: frame.height)
chatTextView.frame = CGRect(x: chatView.frame.origin.x + 10, y: 0, width: chatView.frame.width - 20, height: chatView.frame.height)
}
func setupViews() {
if isTextFromCurrentUser {
chatTextView.frame = CGRect(x: 10, y: 0, width: frame.width - 140, height: frame.height)
chatTextView.backgroundColor = .white
} else {
chatTextView.frame = CGRect(x: frame.width - 150, y: 0, width: frame.width - 140, height: frame.height)
chatTextView.backgroundColor = .blue
}
chatTextView.font = UIFont.systemFont(ofSize: 16)
chatTextView.layer.cornerRadius = 9
chatTextView.clipsToBounds = true
chatTextView.autoresizingMask = UIViewAutoresizing.flexibleHeight
chatTextView.isScrollEnabled = false
contentView.addSubview(chatView)
contentView.addSubview(chatTextView)
}
Chemo,
As I believe its a chat bubble to which you are trying to set the hight for and chat bubble cant have any scroll inside it make sure your textView's scroll is disabled.
Second as Chat bubble should increase its height based on content and there is no height limit use CGFloat.greatestFiniteMagnitude as possible height that you can accommodate while calculating boundingRect
func widthHeight(font: UIFont) -> CGRect {
let constraintRect = CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox
}
Finally make sure there is no contentInset set to the textView. If contentInset set as left 5 and right 5 make sure to subtract 10 (5 + 5) from max width you can accommodate.
As height is the only variable here in equation setting width exactly is the key to get correct height. Make sure you set the line options correct matching ur textViews property.
Suggestion:
UITableView can make use of automatic height for cell and setting scroll disable on textView makes textView to calculate its size based on the text set. I mean textView will respect the implicit size.
As I believe you are creating a chat app where each bubble is a cell, consider more sane option of using UITableView and leverage the benefit of automatic cell height then messing up with collectionView which expects you to provide the size for each item manually.
Pinch of Advice :D
I have personally used bounding rect and managed to calculate the exact height for text after loads of trial and error method. I personally suggest creating a textView instance, setting its property exactly matching the property of textView you have in your storyboard and then set the text you wanna show and use sizeThatFits to get the actual size of textView which is much easier.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let textView = UITextView(frame: CGRect.zero)
//set textView property here
textView.text = self.chatLog[indexPath.row].text
let size = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
return size;
}

UICollectionView edge to edge layout

Goal: edge-to-edge UICollectionView with 2 cells on all size iPhones.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let screenSize = collectionView.bounds
let screenWidth = screenSize.width
print("Zhenya: \(screenWidth)") // prints correct 375, which is half of iPhone 6
let cellEdgeLength: CGFloat = screenWidth / 2.0
return CGSize(width: cellEdgeLength, height: cellEdgeLength)
}
Also
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
}
However on iPhone 6:
Collection Cell attributes:
Collection View attributes:
Update:
func for gradient, that actually gets the right width:
(located at custom UICollectionViewCell class)
func addGradient () {
let gradient = CAGradientLayer()
gradient.frame = gradientView.bounds
let topColor = UIColor(red:0.07, green:0.07, blue:0.07, alpha:1)
let botomColor = UIColor.clear
gradient.colors = [topColor.cgColor, botomColor.cgColor]
if gradientWasRemoved == false {
gradientView.layer.insertSublayer(gradient, at: 0)
} else if gradientWasRemoved == true {
self.addSubview(gradientView)
}
Update 2:
Note: Testing on iPhone 7 Plus.
I found that UICollectionViewFlowLayout overrides cell size crated in sizeForItemAtIndexPath: (seems like it)
With this code:
let flow = UICollectionViewFlowLayout()
let screenSize = collectionView.bounds
let screenWidth = screenSize.width
let cellEdgeLength: CGFloat = screenWidth / 2.0
flow.itemSize = CGSize(width: cellEdgeLength, height: cellEdgeLength)
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
I have this:
Then I decided manual specify cell edge length (half of iPhone7Plust width = 207):
let cellSide: CGFloat = 207
flow.itemSize = CGSize(width: cellSide, height: cellSide)
flow.scrollDirection = UICollectionViewScrollDirection.vertical
flow.minimumInteritemSpacing = 0
flow.minimumLineSpacing = 0
collectionView.collectionViewLayout = flow
I get this:
Found the solution.
Problem was in the inner imageView autolayout settings. I didn't specify them.
Why that little piece of BS was getting on the way of my flow.itemSize, I don't know.
What is more weird to me, is why when manually specified cellEdgeLength as 207 in the second update, all of the sudden, it did override the lack of imageView constraints.

Dynamically changing CollectionView Cell Size Based on Downloaded Images Using Swift

I'm trying to build a collection view layout like Pinterest uses. Most of what is out there is in Objective C, so I've used this RW tutorial: http://www.raywenderlich.com/107439/uicollectionview-custom-layout-tutorial-pinterest
The problem is that the app in the RW tutorial uses local images, whereas I'm trying to base the cell size on images that are downloaded via PinRemoteImage but I cannot get the collectionView to properly lay itself out again once the images are downloaded.
Below is my attempt to modify the extension:
extension PinCollectionViewController : PinterestLayoutDelegate {
// 1
func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath: NSIndexPath,
withWidth width: CGFloat) -> CGFloat {
var pinterestLargestImage = UIImage()
if imageDownloads == 0 {
pinterestLargestImage = imageArray[indexPath.row]
} else {
pinterestLargestImage = UIImage(named: "testPic")!
}
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let rect = AVMakeRectWithAspectRatioInsideRect(pinterestLargestImage.size, boundingRect)
return rect.size.height
}
// 2
func collectionView(collectionView: UICollectionView,
heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
var pinterestLargestImage = UIImage()
if pins.count == imageArray.count {
pinterestLargestImage = imageArray[indexPath.row]
} else { pinterestLargestImage = UIImage(named: "testPic")!}
let annotationPadding = CGFloat(4)
let annotationHeaderHeight = CGFloat(17)
let font = UIFont(name: "AvenirNext-Regular", size: 10)!
let commentHeight = CGFloat(10.0)
let height = annotationPadding + annotationHeaderHeight + commentHeight + annotationPadding
return height
}
}
Then I've tried to call self.collectionView(self.collectionView!, layout: (self.collectionView?.collectionViewLayout)!, sizeForItemAtIndexPath: indexPath) and self.collectionView!.reloadItemsAtIndexPaths([indexPath]) inside cellForRowAtIndexPath once the cell's image is downloaded, but neither properly call these methods to adjust the layout. Can anyone point me in the right direction here?

Resources