Making a UILabel fit nicely into a Collection View Cell - ios

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:

Related

UICollectionView Crashes with Error : Thread 1: EXC_BAD_ACCESS

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

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

set dynamic height of UITableViewCell inside UITableView using UILabel text

Hello guys i have a UITableView in which i have a UITableViewCell which contains UILabel for displaying title and another UILabel for showing description. The Height of UITableViewCell is calculated based on the text of title label and description label.
Following is UITableView method to return height of cell in which i am calculating height of cell based on the text of name and description field.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var height: CGFloat = 60
if empCornerSC.selectedSegmentIndex == 0{
let kra = kraList[indexPath.row]
let maxSize = CGSize(width: 200 , height: 1000)
let nameLabelSize = rectForText(text: kra.kraName!, font: 16, maxSize: maxSize)
let descriptionLabel = rectForText(text: kra.kraDescription!, font: 14, maxSize: maxSize)
height = nameLabelSize.height + descriptionLabel.height
height = height + 20
}
return height
}
Method to calculate height based on text and font, I got this method from Youtube Lets Build That App
func rectForText(text: String, font: CGFloat, maxSize: CGSize) -> CGRect {
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: maxSize, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: font)], context: nil)
}
i am able to get dynamic size for my UITableViewCell but it is inconsistent check the screenshot
as you can see in the image, if the label and description text are large the cell height is large and when the content of lebel are less then the size is also less. I want the cell height related to the size of content. Please help me. Thank you in advance.
Change let maxSize = CGSize(width: 200 , height: 1000) to let maxSize = CGSize(width: labelWidth , height: 1000)
labelWidth should be the maximum width allowed for a particular label. You can use something like [[UIScreen mainScreen] bounds].size.width - [XX](40/50 etc. based on your constraints). In this case 200 seems to be very less as seen in the screenshot attached

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

UILabel subclass - text cut off in bottom despite label being correct height

I have a problem with UILabel subclass cutting off text in the bottom. Label is of proper height to fit the text, there is some space left in the bottom, but the text is still being cut off.
The red stripes are border added to label's layer.
I subclass the label to add edge insets.
override func sizeThatFits(size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
return size
}
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
However, in this particular case the insets are zero.
Turns out the problem was with
self.lineBreakMode = .ByClipping
changing it to
self.lineBreakMode = .ByCharWrapping
Solved the problem
I was facing the same issue with Helvetica Neue Condensed Bold font. Changing label's Baseline property from Align Baselines to Align Centers did the trick for me. You can change this easily in storyboard by selecting your label.
My problem was that the label's (vertical) content compression resistance priority was not high enough; setting it to required (1000) fixed it.
It looks like the other non-OP answers may be some sort of workaround for this same underlying issue.
TL'DR
Probably the property you are looking for is UILabel's baselineAdjustment.
It is needed because of an old UILabel's known bug. Try it:
label.baselineAdjustment = .none
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Explanation
It's a bug
There is some discussions like this one about a bug on UILabel's text bounding box. What we observe here in our case is some version of this bug. It looks like the bounding box grows in height when we shrink the text through AutoShrink .minimumFontScale or .minimumFontSize.
As a consequence, the bounding box grows bigger than the line height and the visible portion of UILabel's height. That said, with baselineAdjustment property set to it's default state, .alignBaselines, text aligns to the cropped bottom and we could observe line clipping.
Understanding this behaviour is crucial to explain why set .alignCenters solve some problems but not others. Just center text on the bigger bounding box could still clip it.
Solution
So the best approach is to set
label.baselineAdjustment = .none
The documentation for the .none case said:
Adjust text relative to the top-left corner of the bounding box. This
is the default adjustment.
Since bonding box origin matches the label's frame, it should fix any problem for a one-lined label with AutoShrink enabled.
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Documentation
You could read more here about UILabel's baselineAdjustmenton official documentation.
Happened for me when providing topAnchor and centerYAnchor for label at the same time.
Leaving just one anchor fixed the problem.
Other answers didn't help me, but what did was constraining the height of the label to whatever height it needed, like so:
let unconstrainedSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
label.heightAnchor.constraint(equalToConstant: label.sizeThatFits(unconstrainedSize).height).isActive = true
Also, sizeThatFits(_:) will return a 0 by 0 size if your label's text field is nil or equal to ""
I ran into this too, but wanted to avoid adding a height constraint. I'd already created a UILabel subclass that allowed me to add content insets (but for the purpose of setting tableHeaderView straight to a label without having to contain it in another view). Using the class I could set the bottom inset to solve the issue with the font clipping.
import UIKit
#IBDesignable class InsetLabel: UILabel {
#IBInspectable var topInset: CGFloat = 16
#IBInspectable var bottomInset: CGFloat = 16
#IBInspectable var leftInset: CGFloat = 16
#IBInspectable var rightInset: CGFloat = 16
var insets: UIEdgeInsets {
get {
return UIEdgeInsets(
top: topInset,
left: leftInset,
bottom: bottomInset,
right: rightInset
)
}
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
return addInsetsTo(size: super.intrinsicContentSize)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return addInsetsTo(size: super.sizeThatFits(size))
}
func addInsetsTo(size: CGSize) -> CGSize {
return CGSize(
width: size.width + leftInset + rightInset,
height: size.height + topInset + bottomInset
)
}
}
This could be simplified just for the font clipping to:
import UIKit
class FontFittingLabel: UILabel {
var inset: CGFloat = 16 // Adjust this
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: UIEdgeInsets(
top: 0,
left: 0,
bottom: inset,
right: 0
)))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(
width: size.width,
height: size.height + inset
)
}
}
I had a vertical UIStackView with a UILabel at the bottom. This UILabel was cutting off the letters that go below the baseline (q, g, y, etc), but only when nested inside a horizontal UIStackView. The fix was to add the .lastBaseline alignment modifier to the outer stack view.
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
aVerticalStackWithUILabelAtBottom, // <-- bottom UILabel was cutoff
UIView(),
someOtherView
])
stackView.axis = .horizontal
stackView.spacing = Spacing.one
stackView.alignment = .lastBaseline // <-- BOOM fixed it
stackView.isUserInteractionEnabled = true
return stackView
}()

Resources