UILabel not breaking lines in UITableViewCell - ios

I'm building an app with a messenger like interface. I use a tableView to accomplish this. Each cell contains a UIView - the message bubble and a UILabel - the message that is nested in the UIView.
It works great on texts of small sizes but for some reason when the UILabel is supposed to break lines it doesn't and it all is in one line. The amount of lines is set to zero.
This is my message handling class:
func commonInit() {
print(MessageView.frame.height)
MessageView.clipsToBounds = true
MessageView.layer.cornerRadius = 15
myCellLabel.numberOfLines = 0
let bubbleSize = CGSize(width: self.myCellLabel.frame.width + 28, height: self.myCellLabel.frame.height + 20)
print(bubbleSize.height)
MessageView.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: bubbleSize.width, height: bubbleSize.height)
if reuseIdentifier! == "Request" {
MessageView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMinXMaxYCorner]
MessageView.backgroundColor = UIColor(red: 0, green: 122/255, blue: 1.0, alpha: 1.0)
} else {
MessageView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner]
MessageView.backgroundColor = UIColor.lightGray
}
}
Cell calling function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if queryCounter % 2 == 0 && indexPath.row % 2 == 0{
cellReuseIdentifier = "Answer"
} else {
cellReuseIdentifier = "Request"
}
let cell:MessageCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MessageCell
cell.myCellLabel.textColor = UIColor.white
cell.myCellLabel.text = self.messages[indexPath.row]
let height = cell.myCellLabel.text!.height(withConstrainedWidth: cell.myCellLabel.frame.width, font: cell.myCellLabel.font)
print(height)
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
The height variable is calculated based on text size. It shows that the text size is calculated normally - accounting for line break.
I was unable to modify the cell height based on this calculation - nothing I tried works.
I think it might be a constraints issue.
My Constraints:
How do I make the lines break? Please help.
EDIT: I just notice that the MessageView.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: bubbleSize.width, height: bubbleSize.height) has no affect what so ever on the message bubbles.

Setting the frame while using autolayout won't work.
I can't say what exactly happens here without the entire context, but some common pitfalls when reusing cells and autolayout are:
Forgetting to set automatic height for your cells (expanding cell in a storyboard manually will override this setting)
Tableview also needs estimatedHeight sometimes to work properly
Sometimes you need to call setNeedsLayout after you add content to
the cell
Check the console and if there are some warnings about breaking constraints, you can easily find issues there.

Try to find the label height based on label width and text font and then set your label height constraint to that.
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil)
return ceil(boundingBox.height)
}
}
something like this:
let textHeight = yourtext.height(withConstrainedWidth: yourlabel.frame.width, font: font)
yourLabel.heightAnchor.constraint(equalToConstant: textHeight).isActive = true

After multiple hours of trying everything I managed to fix it. The problem was with the constraints.
1. As you can see the in this old layout. The UIView was constrained everywhere except the left -> that's where the text goes.
The commonInit() method of the UITableViewCell was called before any text was initialized. That's not good because all of the cell resizing is based on text which was not yet passed to the cell -> Move the method after cell initialization.
let cell:MessageCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MessageCell
cell.myCellLabel.text = self.messages[indexPath.row]
//Before calling commonInit() we need to adjust the cell height.
let height = cell.myCellLabel.text!.heightForView(text: cell.myCellLabel.text!, font: cell.myCellLabel.font, width: self.view.frame.width / 2)
// Then we set the width of the UILabel for it to break lines at 26 characters
if cell.myCellLabel.text!.count > 25 {
tableView.rowHeight = height + 20
cell.myCellLabel.widthAnchor.constraint(equalToConstant: cell.frame.width / 2).isActive = true
cell.updateConstraints()
}
// Calling commonInit() after adjustments
cell.commonInit()
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
Then we need to update the constraints so that the UIView and UILabel resize with the cell height.
Done. Now it works as needed. Thank you for all of the suggestions!

Related

Swift: Continuous resizing of tableViewCell while user types in a UITextView

Introduction
Context:
I'm creating one of my first apps but I've ran into an issue I cannot figure out.
I have tableView with cells packed with quite a few UIElements. All constraints are done using the EasyPeasy library which basically just sets auto layout constraints ( I have tried setting them manually also). The UITextView in question is constrained by various numbers to the left, right, top and bottom, I have no constraints on it for height or width.
in cellForRowAt indexPath: I set the textView delegate for each cells textView to self, using a delegate property declared within the cells custom class. I also tag every textView with its cells indexPath.row (gives textView.tag integer in textViewDidChange method).
Issue/acknowledgments:
After browsing SO a lot I've found a few questions alike this but I have not been able to make them work for me, I have implemented parts of them that felt logic to my case. I believe the problem differencing my situation from those questions lies in that for my cell design to work the cells has to have a height of itemHeight or higher.
I have noticed that as I type into the textview the textview itself increases in height (even below the cells border but its not visible as it reaches that point), however the cell itself doesn't resize.
I've tried with a cell that only contains a textView so the problem must lie in the textViewDidchange or heightForRowAt indexPath methods.
Question:
What am I doing wrong here? Why doesn't the cells height change dynamically as I type in the textView?
Code:
func textViewDidChange(_ textView: UITextView) {
var newframe = textView.frame
newframe.size.height = textView.contentSize.height - textView.frame.size.height + itemHeight[textView.tag]
textView.frame = newframe
let ndxPath = IndexPath(row: textView.tag, section: 0)
let cell = tableView.cellForRow(at: ndxPath) as! EventsCell
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: textView.frame.height)
tableView.beginUpdates()
tableView.setNeedsLayout() //have tried without this line
tableView.layoutIfNeeded() //have tried without this line
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if UITableViewAutomaticDimension > itemHeight[indexPath.row] {
return UITableViewAutomaticDimension
} else {
return itemHeight[indexPath.row]
}
}
TextView constraints:
let containerView : UIView = {
let cv = UIView(frame: .zero)
cv.backgroundColor = .white
cv.translatesAutoresizingMaskIntoConstraints = false
cv.layer.cornerRadius = 7
return cv
}()
let eventText : GrowingTextView = { // GrowingTextView is a extension to a regular UITextView
let tv = GrowingTextView()
tv.allowsEditingTextAttributes = true
tv.isScrollEnabled = false
var delegate: UITextViewDelegate?
tv.textContainerInset = UIEdgeInsetsMake(1, 1, 0, 1)
tv.autoresizingMask = .flexibleHeight
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
containerView.addSubview(eventText)
contentView.addSubview(containerView)
containerView .easy.layout([Height(CGFloat(95 * itemCount)), Left(8), Right(8)])
eventText .easy.layout([Left(77), Right(5), Top(90), Bottom(4)])
}
Thanks for reading my post.
The constraints that determine the height should be laid out in such a way that the textView is attached directly to the top and bottom of the contentView or to views which are connected to the top and bottom of the contentView, so that autolayout can make out the height by connecting the constraints.
Make sure that you do not mention a height for the textView and disable scrolling. Let automatic dimension take care of all that.
Now all you need to do is call tableView.beginUpdates() and tableView.endUpdates() on textViewDidChange
Here is my repo which demonstrates the same.
OP Edit:
You should store the additional height that you add in a variable in the cell class so the cells can reload an appropriate height when the tableVIew is reloaded.
You should also change textViewDidChange method
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: textView.frame.height)
to
let newFrame = ”originalCellHeight” - ”originalTextViewHeight” + textView.contentSize.height
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y, width: cell.frame.width, height: newFrame )`

iOS - AccessoryView changes the size of custom separator

I have a grouped tableview and at first I wanted to remove the top and bottom separator. I looked around and found a solution that worked great form me. It was by adding a UIView to the bottom of the cell to act as the separator. Now it looks amazing. But the problem I faced is when I set the AccessoryView of the cell , because for some reason when I select the cell , my custom separator change its width . Here's a representation of the problem :
this is when not selected
this is when selected :
Note that the custom separator already has constraint .
Any suggestions please ?
Edit:
Here's how I set the accessoryView :
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let cell = tableView.cellForRow(at: indexPath) as? TableViewCell {
let label = UILabel()
label.text = "x"
label.sizeToFit()
label.font = UIFont.systemFont(ofSize: 13, weight: .medium)
if(cell.accessoryView == nil){
cell.accessoryView = label
}else{
cell.accessoryView = nil
}
}
}
that's it and everything else is just a simple tableview with rows and section , I also set the separator to none in storyboard.
Edit 2 :
This is the separator constraints :
You need to change your trailing constraint. Instead of ContentView add trailing constraint to cell. Remove trailing constraint on separator view and add constraints as in image.
In iOS 13+, I had to change the code to draw bottom border on the cell instead of the cell's contentView. I do it in awakeFromNib in the Cell class.
override func awakeFromNib() {
super.awakeFromNib()
addBorder(borderType: .bottom)
}
func addBorder(borderType: BorderType, width: CGFloat = 1.0, color: UIColor = .darkGray) {
// figure out frame and resizing based on border type
var autoresizingMask: UIView.AutoresizingMask
var layerFrame: CGRect
switch borderType {
case .left:
layerFrame = CGRect(x: 0, y: 0, width: width, height: self.bounds.height)
autoresizingMask = [ .flexibleHeight, .flexibleRightMargin ]
case .right:
layerFrame = CGRect(x: self.bounds.width - width, y: 0, width: width, height: self.bounds.height)
autoresizingMask = [ .flexibleHeight, .flexibleLeftMargin ]
case .top:
layerFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: width)
autoresizingMask = [ .flexibleWidth, .flexibleBottomMargin ]
case .bottom:
layerFrame = CGRect(x: 0, y: self.bounds.height - width, width: self.bounds.width, height: width)
autoresizingMask = [ .flexibleWidth, .flexibleTopMargin ]
}
// look for the existing border in subviews
var newView: UIView?
for eachSubview in self.subviews {
if eachSubview.tag == borderType.rawValue {
newView = eachSubview
break
}
}
// set properties on existing view, or create a new one
if newView == nil {
newView = UIView(frame: layerFrame)
newView?.tag = borderType.rawValue
self.addSubview(newView!)
} else {
newView?.frame = layerFrame
}
newView?.backgroundColor = color
newView?.autoresizingMask = autoresizingMask
}

How to change detailTextLabel height programmatically

I programmed an array into a tableView. When the cell utilizes more than one line for the detailTextLabel, the space in between the lines is small. I would like to know if there is any way to increase this height programmatically? Here is sample code I am using for the array.
cell.textLabel?.text = self.filtered[indexPath.row].coptic
cell.detailTextLabel?.text = self.filtered[indexPath.row].english
cell.textLabel?.font = UIFont(name:"CS Avva Shenouda", size:30)
cell.detailTextLabel?.font = UIFont(name: "Constantia", size:25)
cell.textLabel?.numberOfLines = 0
cell.detailTextLabel?.numberOfLines = 0
cell.detailTextLabel?.textColor = UIColor.darkGray
return cell
I'm just putting my logic, not whole code.
You can get height of string by below code
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(boundingBox.height)
}
Change cellForRowAtIndexPath Method
cell.detailTextLabel?.numberOfLines = 0
let height = height(withConstrainedWidth:200, font:YourFont) // change width and font as per your requirement
cell.detailTextLabel?.frame = CGRect(x: cell.detailTextLabel?.frame.origin.x, y: cell.detailTextLabel?.frame.origin.y, width: cell.detailTextLabel?.frame.size.width, height: height)
You can manage cell height according to detailTextLabel height
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 50 // You should put your code or logic that dynamic height based on heigh of label.
}
The table view needs to have an estimatedRowHeight and the tableView height as UITableViewAutomaticDimension.

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

sizeToFit not working on a programmatic tableview cell

I have a basic custom cell with a name label on the left and a price label on the right both set inside another view to customise the spacing. I want the price to change width to whatever the price is and not have it set but when i use sizetofit on the price in the cells init or the cellForRow at function nothing happens. I have looked around but cant see how to get it to work. I cant get the text size when in the init of a cell but it doesnt seem right to be setting the label size within cellForRowAt.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SplitterCarouselItemTableViewCell = tableView.dequeueReusableCell(withIdentifier: "splitterCarouselItemTableViewCell") as! SplitterCarouselItemTableViewCell
var item = ((allBillSplitters[tableView.tag].items)?.allObjects as! [Item])[indexPath.row]
if allBillSplitters[tableView.tag].isMainBillSplitter {
getMainBillSplitterItems(splitter: allBillSplitters[tableView.tag])
item = mainBillSplitterItems[indexPath.row]
}
let count = item.billSplitters?.count
if count! > 1 {
cell.name!.text = "\(item.name!)\nsplit \(count!) ways"
cell.price!.text = "£\(Double(item.price)/Double(count!))"
} else {
cell.name!.text = item.name!
cell.price!.text = "£\(item.price)"
}
return cell
}
and heres my cell:
import UIKit
class SplitterCarouselItemTableViewCell: UITableViewCell {
var name: UILabel!
var price: UILabel!
var view: UIView!
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:)")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: "splitterCarouselItemTableViewCell")
self.setupViews()
}
func setupViews() {
let width = Int(UIScreen.main.bounds.width * 0.88)
let height = Int(self.bounds.height)
self.backgroundColor = .clear
self.frame = CGRect(x: 0, y: 0, width: width, height: 45)
view = UIView(frame: CGRect(x: 0, y: 2, width: width, height: height - 4 ))
view.backgroundColor = UIColor.white.withAlphaComponent(0.5)
price = UILabel(frame: CGRect(x: width - 80, y: 0, width: 75, height: height))
price.font = UIFont.systemFont(ofSize: 15.0)
price.backgroundColor = .yellow
price.textAlignment = .right
name = UILabel(frame: CGRect(x: 5, y: 0, width: Int(price.frame.width), height: height))
name.backgroundColor = .red
name.font = UIFont.systemFont(ofSize: 15.0)
name.numberOfLines = 0
view.addSubview(name)
view.addSubview(price)
contentView.addSubview(view)
}
}
Any help would be great, im sure im missing something basic.
Ive added the yellow and red backgrounds for visibility in the screen shot.
You need to calculate the width of the price label based on text. Below code will help you to find the width
extension String {
func widthWithConstrainedHeight(height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height )
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.width
}
}
Use above function to find string width and use that width to create frame of price label.
I'm unclear if you want the frame size to adjust or the font size. This answer assumes the latter, i.e. that you want a fixed frame width and a font that shrinks as needed to fit....
Try enabling adjustsFontSizeToFitWidth,
price = UILabel(frame: CGRect(x: width - 80, y: 0, width: 75, height: height))
price.font = UIFont.systemFont(ofSize: 15.0)
price.backgroundColor = .yellow
price.textAlignment = .right
price.adjustsFontSizeToFitWidth = true
this should shrink the font to fit the width of the label.

Resources