Originally my CollectionView was working fine, but I wanted to adjust the width of the item in CollectionView according to the width of the TextLabel in CollectionView, so I added some code and then crashed when the program was initialized:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OnedaySubTodoCell", for: indexPath) as! SubCell
let width = 28 + cell.subNameLabel.bounds.size.width
print("Width: \(width)")
return CGSize(width: width, height: 20)
}
This is an error report and it show in class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x7a00b0018)
This is output:
Width: 88.0
(lldb)
My class has inherited UICollectionViewDelegateFlowLayout,I want to know where the problem is.
As #rmaddy and #Prashant pointed out,
You should not use cellForItemAt in sizeForItemAT because
sizeForItemAt is called BEFORE initializing the cell in
cellForItemAt
And most probably that's the reason for your crash. Coming towards Solution.
I was faced with a similar problem (had to dynamically manage height) and what i did is something like
Calculate the estimated width of your label based on text. use the following string extension
//calculates the required width of label based on text. needs height and font of label
extension String {
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
return ceil(boundingBox.width)
}
}
Now, inside sizeForItemAt
//put actual lblHeight here
let lblHeight = Put_actual_label_height_here // e.g 30
//put actual label font here
let lblFont = Put_actual_label_font_here //e.g UIFont.boldSystemFont(ofSize: 20)
//calculate required label width
let lblRequiredWidth = yourLabel's_Text_String.width(withConstrainedHeight: lblHeight, font: lblFont)
//you may want to return size now
let height = yourItemsHeight
return CGSize(width: lblRequiredWidth, height: height)
Now that you have got the required width of your label, you can adjust the size of the item based on label's width.
Hope that helps. Let me know if you need any help. Thanks
Related
I have tried to create self-sizing collectionView cell, but my label doesn't want to expand his height and become multilines, instead the label just goes in one single line and my cell goes beyond of collectionView frame. I have created my own custom UICollectionViewFlowLayout and tried to handle all stuff there. What have I done wrong? Looking forward for your answers, thanks!
My implementation:
class VerticalLayout:UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
self.scrollDirection = .vertical
self.minimumLineSpacing = 0
self.minimumInteritemSpacing = 0
self.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 173)
self.sectionInset = .zero
}
}
You must count text height and pass it to your collectionview delegate method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemHeight: CGFloat = estimatedHeightForText(text: items[indexPath.item].text, fontSize: fontSize, fontWeight: fontWeight, width: widthForText) + someRawValue // here count height for cell
var itemWidth: CGFloat = someRawVal
return CGSize(width: itemWidth, height: itemHeight)
}
Count it here
func estimatedHeightForText(text: String, fontSize: CGFloat, fontWeight: UIFont.Weight, width: CGFloat) -> CGRect {
let size = CGSize(width: width, height: 1000)
let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight)
return NSString(string: text).boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
}
UPD. widthForText - this is important value for text width in your cell. Count it properly
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)
}
I have collection view cells with equal width and height in a flow layout. They contain a single UILabel, which numberOfLines property is set to 0. The constraints of the label are:
The cell is made circular:
cell.layer.cornerRadius = cell.frame.width / 2
cell.clipsToBounds = true
I increase the size of each cell based on the label's text size. However, it's width and height can't be greater than 150. Here is how I determine the size of each cell:
private func estimatedFrameForText(text: String) -> CGRect {
let size = CGSize(width: 100, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)], context: nil)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let hashLabelText = textArray[indexPath.item]
let textSize = estimatedFrameForText(text: hashLabelText)
let width = min(150, textSize.width + 20)
let height = width
return CGSize(width: width, height: height)
}
With this I get the following result (I'm showing the part where the cells' width and height are equal to 150).:
As you can see, when the cell hits its maximum possible size and the text of the labels continues to increase, at some point, the text gets off the visible part of the cells. I understand why this happens (the layout debugger shows this clearly), however I can't find a solution to the problem, yet.
What I want is that the edges of the label remain visible no matter what is the size of the cell. The text's tail can be truncated if the cell reaches its maximum size, but the text continues to increase.
I've tried to increase the space between the label and the bounds of the cells but that affects how the text looks in the smallest cells. I've also tried to change the minimum font scale of the label, but again, without a success.
You have to use UIEdgeInsets for this, Create a class for UILabel:
import UIKit
class UILabelDraw: UILabel {
override func drawText(in rect: CGRect) {
let insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
super.drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}
}
Use this class as a Label class like below:
Output of UIEdgeInsets is below:
I was trying wrap test into enclosing box by means of boundingrectwithsize function.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let messageText = messages?[indexPath.item].text {
let size = CGSize(width:view.frame.width,height: 1000)
let options = NSStringDrawingOptions.usesLineFragmentOrigin.union(.usesFontLeading)
let estimatedFrame = NSString(string: messageText).boundingRect(with: size, options: options, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 18)], context: nil)
return CGSize(width: view.frame.width,height: estimatedFrame.height)
}
return CGSize(width:view.frame.width,height: 100)
}
}
But the output was something weird and i am very confused
why it did always cutting last string? It seemed to me i was using right NSString.DrawingOptions and especially usesFontLeading calculates the size but why it cut the last string?
UITextView adds padding by default. Sizing using NSString sizing methods don't take this into account. You can either turn off the padding using:
myTextView.textContainerInset = UIEdgeInsets.zero
myTextView.textContainer.lineFragmentPadding = 0
Or add them to your size:
CGSize(width: view.frame.width,height: estimatedFrame.height + myTextView.textContainerInset.top + myTextView.textContainerInset.bottom)
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;
}