Swift: tableview cell content is added again and again with each reload? - ios

Ok, I am fairly this Objective C question had the same problem = Cell Label text overlapping in cells but I haven't found any answers in Swift. Im also very new to tableviews/cells and would just like to know the proper way to do this as clearly Im doing it wrong-
I have custom cells in my tableview that I created in storyboard. I need to add the content of my cells (labels, etc) programmatically. I have done this here -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("eventCell", forIndexPath: indexPath) as! EventTableCellTableViewCell
// cell.eventTitle.text = names[indexPath.row]
// cell.eventDescription.text = descriptions[indexPath.row]
cell.contentView.clipsToBounds = false
//cell UIX
let eventTitleLabel = UILabel()
let dateLabel = UILabel()
let authorLabel = UILabel()
let locationLabel = UILabel()
let categoryView = UIImageView()
//border
let botBorder: CALayer = CALayer()
botBorder.frame = CGRectMake(0.0, cell.frame.height-1, cell.frame.width, 1.0)
botBorder.backgroundColor = colorWithHexString("#C5C7C9").CGColor
//initalize cell items
eventTitleLabel.text = names[indexPath.row]
eventTitleLabel.frame = CGRectMake(0, 0, cell.frame.width * 0.5, cell.frame.height * 0.3)
eventTitleLabel.tag = indexPath.row
eventTitleLabel.textAlignment = .Left
eventTitleLabel.font = UIFont(name: "Montserrat-Bold", size: screenSize.height * (24/568))
eventTitleLabel.textColor = UIColor.blackColor()
eventTitleLabel.center = CGPointMake(cell.contentView.frame.width * 0.35, cell.contentView.frame.height * 0.35)
dateLabel.textColor = colorWithHexString("#C5C7C9")
let dateString = "\(dates[indexPath.row]) \(times[indexPath.row])"
dateLabel.text = dateString
dateLabel.frame = CGRectMake(0, 0, cell.frame.width * 0.5, cell.frame.height * 0.3)
dateLabel.tag = indexPath.row
dateLabel.textAlignment = .Left
dateLabel.font = UIFont(name: "Montserrat-Regular", size: screenSize.height * (10/568))
dateLabel.center = CGPointMake(cell.contentView.frame.width * 0.35, cell.contentView.frame.height * 0.6)
//for setting bottom label
//Code sets label (yourLabel)'s text to "Tap and hold(BOLD) button to start recording."
let boldAttribute = [
//You can add as many attributes as you want here.
NSFontAttributeName: UIFont(name: "Montserrat-Bold", size: 11.0)!]
let regularAttribute = [
NSFontAttributeName: UIFont(name: "Montserrat-Regular", size: 11.0)!]
let beginningAttributedString = NSAttributedString(string: authors[indexPath.row], attributes: boldAttribute )
//let boldAttributedString = NSAttributedString(string: locationNames[indexPath.row], attributes: boldAttribute)
let boldAttributedString = NSAttributedString(string: "Monterey, CA USA", attributes: regularAttribute)
let fullString = NSMutableAttributedString()
fullString.appendAttributedString(beginningAttributedString)
fullString.appendAttributedString(NSAttributedString(string: " ", attributes: regularAttribute)) //space
fullString.appendAttributedString(boldAttributedString)
//------
authorLabel.attributedText = fullString
authorLabel.textColor = colorWithHexString("#C5C7C9")
authorLabel.frame = CGRectMake(0, 0, cell.frame.width, cell.frame.height * 0.3)
authorLabel.tag = indexPath.row
authorLabel.textAlignment = .Left
authorLabel.center = CGPointMake(cell.contentView.frame.width * 0.5, cell.contentView.frame.height * 0.8)
categoryView.frame = CGRectMake(0, 0, screenSize.width * (50/screenSize.width), screenSize.width * (50/screenSize.width))
categoryView.layer.cornerRadius = categoryView.frame.width * 0.5
categoryView.center = CGPointMake(cell.contentView.frame.width * 0.7, cell.contentView.frame.height * 0.35)
categoryView.backgroundColor = colorWithHexString("#3dccff")
cell.contentView.addSubview(categoryView)
cell.contentView.addSubview(eventTitleLabel)
cell.contentView.addSubview(dateLabel)
cell.contentView.addSubview(locationLabel)
cell.contentView.addSubview(authorLabel)
cell.contentView.layer.addSublayer(botBorder)
print("called cell")
return cell
}
And this works the first time. However I learned from the print to console that this is called every time you scroll, and also after I add new items that take up new cells in my tableview. When that happens I get this overlapping -
How do I fix this? I looked also at TableViewCell is piled up and appear again and again and tried putting cell.contentView.removeFromSuperView() at the beginning of this function so it would clear out the old content but that resulted in absolutely nothing showing up.
What is the right way to add content programmatically?

The tableview cells are recycled, therefore each time a cell is presented its going to have whatever you put in it last, you will need to appropriately handle a cell that comes back filled with the labels you have put in. Probably should have some kind of init method of the cell that is called only once per new cell, and is ignored when the cell is recycled, then just edit the labels and what ever else as normal. This kind of functionality should be built into the cells custom class itself instead of inside the cellForRowAtIndexPath

Related

Programmatically center UIImage inside parent view vertically

I am on Swift 5.
The goal is to center a UIImageView vertically inside a view. Currently it looks like
Note all the image bubbles are running off of the cell.
This is the code that lead to this:
let imageView = UIImageView()
let width = self.frame.width
let height = self.frame.height
let img_width = height //* 0.8
let img_height = height
let y = (height - img_height)/2
let x = width*0.05
imageView.frame = CGRect(
x: x
, y: CGFloat(y)
, width: img_width
, height: img_height
)
let rounded = imageView
.makeRounded()
.border(width:1.0, color:Color.white.cgColor)
self.addSubview(rounded)
The imageView extension functions are:
func makeRounded() -> UIImageView {
self.layer.borderWidth = 0.5
self.layer.masksToBounds = false
self.layer.borderColor = Color.white.cgColor
self.layer.cornerRadius = self.frame.width/2
self.clipsToBounds = true
// see https://developer.apple.com/documentation/uikit/uiview/contentmode
self.contentMode = .scaleAspectFill
return self
}
func border( width: CGFloat, color: CGColor ) -> UIImageView{
self.layer.borderWidth = width
self.layer.borderColor = color
return self
}
Which is very vanilla.
This is odd because I laid out the textview vertically in the exact same way, that is: (parentHeight - childHeight)/2, and it is centered. You can see it in the blue text boxes in cell two and three.
____ EDIT _______
This is how I laid out the cell
let data = dataSource[ row - self._data_source_off_set ]
let cell = tableView.dequeueReusableCell(withIdentifier: "OneUserCell", for: indexPath) as! OneUserCell
// give uuid and set delegate
cell.uuid = data.uuid
cell.delegate = self
// render style: this must be set
cell.hasFooter = false //true
cell.imageSource = data
cell.headerTextSource = data
cell.footerTextSource = data
// color schemes
cell.backgroundColor = Color.offWhiteLight
cell.selectionColor = Color.graySecondary
Add these constraints to you imageView and remove frame and its calculations
self.contentView.addSubview(rounded)
self.mimageView.translatesAutoresizingMaskIntoConstraints = false
self.mimageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor,constant: 20).isActive = true
self.mimageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
self.mimageView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true
self.mimageView.widthAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true

How to set width of UIImageView based on UILabel row width

I have recently begun work on an app using Swift in Xcode and am trying to create a text bubble. To do this, I need to get the width of the longest row of text in a multi-row UILabel. For example, if I have this text (I automatically set line breaks after a certain length):
Hello there, this is
an example piece of text
I would like to return the width of the text in the second row. I have already tried using sizeToFit() which would drastically simplify my work, but because of my other code, this is not an option as it causes other problems (my code is below). Is there a purely programmatic way to get this value without using sizeToFit()? Any help would be much appreciated. My code:
bubbleContents.text = textMessage
bubbleContents.numberOfLines = 0
bubbleContents.lineBreakMode = .byWordWrapping
bubbleContents.bounds.size.width = 2000
var widthText = bubbleContents.intrinsicContentSize.width
bubbleContents.bounds.size.width = 266
print(textMessage)
print(widthText)
if widthText > 266 {
let numRows = Int(widthText/266)
print(numRows)
//bubbleContents.frame.origin.y += CGFloat((Double(numRows)*10.25))
var currentHeight = 44.0
currentHeight += Double((Double(numRows)*20.5))
bubbleContents.bounds.size.height = CGFloat(currentHeight)
heightOfCell = Double(currentHeight)
let originalTransform = self.bubbleContents.transform
let scaledTransform = originalTransform
let scaledAndTranslatedTransform = scaledTransform.translatedBy(x: 0, y: CGFloat(Double(numRows)*20.5))
//self.bubbleContents.transform = scaledAndTranslatedTransform
}
else {
heightOfCell = 44.0
}
bubble.frame = CGRect(x: 0, y: 0, width: Double(widthText + 30), height: heightOfCell - 4)
bubbleContents.center.y = bubble.center.y
Here is an image of what my current text bubbles look like:
You can use NSAttributedString,boundingRect(with:options:context:) method, begin by creating NSAttributedString with attributes such as font of your UILabel
let attributes: [NSAttributedString.Key : Any] = [.font: bubbleContents.font]
let atStr = NSAttributedString(string: textMessage, attributes: attributes)
Now use atStr.boundingRect(with:options:context:) method, like so:
let bounds = CGSize(width: 266.0, height: .greatestFiniteMagnitude)
let bubbleSize = atStr.boundingRect(with: bounds, options: [.usesLineFragmentOrigin, .usesFontLeading, .usesDeviceMetrics], context: nil).size
Usage:
bubble.frame.size.width = bubbleSize.width
bubble.frame.size.height = max(bubbleSize.height, 44.0)

UILabel not breaking lines in UITableViewCell

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!

UITableViewCell height incorrect, sizeToFit sizes incorrectly

I am attempting to create a custom UITableViewCell, and having issues with the cell frame having the proper height. This is troubling because the cell sizes correctly for iPhones 4s/5s running iOS 8.4, but not for iPhones 6/6+ running the same OS.
Chaos ensues around calling sizeToFit on messageLabel. Some of the labels almost appear to have extra, blank lines below, but clearly are not as tall as the cell makes them out to be.
Below is the custom cell. The label that appears to cause the trouble is the messageLabel. To view the frames of the labels, let borders = true
//
// NotesTableViewCell.swift
// urchin
//
// Created by Ethan Look on 6/17/15.
// Copyright (c) 2015 Tidepool. All rights reserved.
//
import Foundation
import UIKit
let noteCellHeight: CGFloat = 128
let noteCellInset: CGFloat = 16
let labelSpacing: CGFloat = 6
class NoteCell: UITableViewCell {
let borders = false
var cellHeight: CGFloat = CGFloat()
let usernameLabel: UILabel = UILabel()
let timedateLabel: UILabel = UILabel()
var messageLabel: UILabel = UILabel()
func configureWithNote(note: Note) {
usernameLabel.text = note.user!.fullName
usernameLabel.font = UIFont(name: "OpenSans-Bold", size: 17.5)!
usernameLabel.textColor = UIColor.blackColor()
usernameLabel.sizeToFit()
let usernameX = noteCellInset
let usernameY = noteCellInset
usernameLabel.frame.origin = CGPoint(x: usernameX, y: usernameY)
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEEE M.d.yy h:mm a"
var dateString = dateFormatter.stringFromDate(note.timestamp)
dateString = dateString.stringByReplacingOccurrencesOfString("PM", withString: "pm", options: NSStringCompareOptions.LiteralSearch, range: nil)
dateString = dateString.stringByReplacingOccurrencesOfString("AM", withString: "am", options: NSStringCompareOptions.LiteralSearch, range: nil)
timedateLabel.text = dateString
timedateLabel.font = UIFont(name: "OpenSans", size: 12.5)!
timedateLabel.textColor = UIColor.blackColor()
timedateLabel.sizeToFit()
let timedateX = contentView.frame.width - (noteCellInset + timedateLabel.frame.width)
let timedateY = usernameLabel.frame.midY - timedateLabel.frame.height / 2
timedateLabel.frame.origin = CGPoint(x: timedateX, y: timedateY)
messageLabel.frame.size = CGSize(width: contentView.frame.width - 2 * noteCellInset, height: CGFloat.max)
let hashtagBolder = HashtagBolder()
let attributedText = hashtagBolder.boldHashtags(note.messagetext)
messageLabel.attributedText = attributedText
messageLabel.adjustsFontSizeToFitWidth = false
messageLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
messageLabel.numberOfLines = 0
messageLabel.sizeToFit()
let messageX = noteCellInset
let messageY = usernameLabel.frame.maxY + 2 * labelSpacing
messageLabel.frame.origin = CGPoint(x: messageX, y: messageY)
contentView.addSubview(usernameLabel)
contentView.addSubview(timedateLabel)
contentView.addSubview(messageLabel)
cellHeight = noteCellInset + usernameLabel.frame.height + 2 * labelSpacing + messageLabel.frame.height + noteCellInset
if (borders) {
usernameLabel.layer.borderWidth = 1
usernameLabel.layer.borderColor = UIColor.redColor().CGColor
timedateLabel.layer.borderWidth = 1
timedateLabel.layer.borderColor = UIColor.redColor().CGColor
messageLabel.layer.borderWidth = 1
messageLabel.layer.borderColor = UIColor.redColor().CGColor
self.contentView.layer.borderWidth = 1
self.contentView.layer.borderColor = UIColor.blueColor().CGColor
}
self.contentView.frame.size = CGSize(width: self.contentView.frame.width, height: cellHeight)
}
}
And heightForRowAtIndexPath:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell = NoteCell(style: .Default, reuseIdentifier: nil)
cell.configureWithNote(notes[indexPath.row])
return cell.cellHeight
}
The project is open source and on Github, so feel free to clone the repository and check out all of the code yourself.
Thank you!
Unfortunately, you can't do it that way because tableView(_:heightForRowAtIndexPath) is called first and the value you return is used to create the cell that you will dequeue in tableView(_:cellForRowAtIndexPath). The cell can't set its own size because by the time it could do so (e.g. awakeFromNib or prepareForResuse), the table view will already have a height value for it. There are some whacky workarounds for this that I've used, but it's easier to just use self-sizing table view cells.
Check it:
http://www.appcoda.com/self-sizing-cells/
Instead of creating an entirely new cell in heightForRowAtIndexPath:, I simply create the UI elements that determine the cell height (usernameLabel and messageLabel), size them appropriately with sizeToFit, then do a simple calculation to determine the cell height.
By doing this, I never create a new cell which is later dequeued.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let usernameLabel = UILabel(frame: CGRectZero)
usernameLabel.font = UIFont(name: "OpenSans-Bold", size: 17.5)!
usernameLabel.text = notes[indexPath.row].user!.fullName
usernameLabel.sizeToFit()
let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.frame.width - 2*noteCellInset, height: CGFloat.max))
let hashtagBolder = HashtagBolder()
let attributedText = hashtagBolder.boldHashtags(notes[indexPath.row].messagetext)
messageLabel.attributedText = attributedText
messageLabel.adjustsFontSizeToFitWidth = false
messageLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
messageLabel.numberOfLines = 0
messageLabel.sizeToFit()
let cellHeight = noteCellInset + usernameLabel.frame.height + 2 * labelSpacing + messageLabel.frame.height + noteCellInset
return cellHeight
}

Automatically Resize UILabel

In Xcode 6 Beta 5, I had a chat interface that looks like the iOS 7 messages app, where the UILabel that the text was inside sized to the width of the text itself. When I updated to Beta 6, I noticed an option for UILabel in interface builder that I hadn't noticed before:
When I have the explicit width set, the width doesn't change at all based on the width of the text. When I uncheck explicit, the width of the text is at least 234, so it expands out of the view.
I am using a UICollectionView inside of a UIViewController, and here is my cell for item at index path method:
func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let defaults = NSUserDefaults.standardUserDefaults()
let row = indexPath.row
var cell: UICollectionViewCell
let path = UIBezierPath()
let object: AnyObject = (messages[row] as NSDictionary).objectForKey("user_id")!
let uid: AnyObject = defaults.objectForKey("user_id")!
if "\(object)" == "\(uid)" {
cell = collectionView.dequeueReusableCellWithReuseIdentifier(right_chat_bubble, forIndexPath: indexPath) as UICollectionViewCell
path.moveToPoint(CGPointMake(0, 0))
path.addLineToPoint(CGPointMake(0, 10))
path.addLineToPoint(CGPointMake(12, 5))
path.addLineToPoint(CGPointMake(0, 0))
}
else {
cell = collectionView.dequeueReusableCellWithReuseIdentifier(left_chat_bubble, forIndexPath: indexPath) as UICollectionViewCell
path.moveToPoint(CGPointMake(0, 5))
path.addLineToPoint(CGPointMake(12, 10))
path.addLineToPoint(CGPointMake(12, 0))
path.addLineToPoint(CGPointMake(0, 5))
}
let initial_view = cell.viewWithTag(101) as UILabel
initial_view.layer.cornerRadius = 20
initial_view.layer.masksToBounds = true
let name = (messages[row] as NSDictionary).objectForKey("name")! as String
let name_array = name.componentsSeparatedByString(" ")
let first_initial = name_array[0]
let last_initial = name_array[1]
let first_char = first_initial[0]
let last_char = last_initial[0]
let initials = first_char + last_char
initial_view.text = initials
let circle: UIView = cell.viewWithTag(103)! as UIView
let mask = CAShapeLayer()
mask.frame = circle.bounds
mask.path = path.CGPath
circle.layer.mask = mask
let message = cell.viewWithTag(102) as ChatLabel
message.enabledTextCheckingTypes = NSTextCheckingType.Link.toRaw()
message.delegate = self
message.text = (messages[row] as NSDictionary).objectForKey("content")! as String
message.layer.cornerRadius = 15
message.layer.masksToBounds = true
message.userInteractionEnabled = true
return cell
}
take a look at sizeThatFits: and sizeToFit:
https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIView_class/index.html#//apple_ref/occ/instm/UIView/sizeThatFits:
https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIView_class/index.html#//apple_ref/occ/instm/UIView/sizeToFit
A UILabel can call sizeThatFits like :
myLabelLbl.text = #"some text"
CGSize maximumLabelSize = CGSizeMake(200, 800)
CGSize expectedSize = [myLabelLbl sizeThatFits:maximumLabelSize]
myLabelLbl.frame = CGRectMake(0, 0, expectedSize.width, expectedSize.height) //set the new size

Resources