Change TableViewCell height depending on label.text? - ios

How can I change the cell height to make the UILabel fit? I am not using Auto-Layout in my project.
Also, the TableViewCell text is set in the cellForRowAtIndexPath.
Code:
var commentsArray: [String] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableViewCell;
if self.commentsArray.count > indexPath.row{
cell.commentsText.text = commentsArray[commentsArray.count - 1 - indexPath.row]
cell.commentsText.font = UIFont.systemFontOfSize(15.0)
}
return cell
}
Any ideas?

The above comment of using heightForRowAtIndexPath is acceptable when not using auto layout, (though I highly recommend getting used it).
Calculating the height of a label when it's populated with a certain string can be done using the methods described in this post: How to calculate UILabel height dynamically?

1) You can use a method to set constraints and you can modify the height of the cell in this way:
(Example with a label called book_title in a custom UITableViewCell)
fune setupComponents(){
self.addSubview(book_title)
book_title.topAnchor.constraintEqualToAnchor(self.topAnchor, constant: 5).active = true
book_title.leftAnchor.constraintEqualToAnchor(self.leftAnchor, constant: 10).active = true
book_title.rightAnchor.constraintEqualToAnchor(self.rightAnchor).active = true
book_title.widthAnchor.constraintEqualToAnchor(self.widthAnchor, constant: -20).active = true
book_title.heightAnchor.constraintEqualToConstant(90).active = true
book_title.numberOfLines = 0
book_title.lineBreakMode = NSLineBreakMode.ByWordWrapping
}
2) You have to call this method inside these:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setupComponents()
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .Subtitle, reuseIdentifier: "CellId")
setupComponents()
}

Related

Custom TableViewCells do not show up

So I am pretty new to iOS development. I try to create everything programmatically so my Storyboard is empty. I'm currently trying to get a TableView with custom cells. The TableView is running and looking fine when I use the standard UITableViewCell. I created a very simple class called "GameCell". Basically, I want to create a cell here with multiple labels and maybe some extra UIObjects in the future (imageView etc.). For some reason, the custom cells do not show up.
Game cell class:
class GameCell: UITableViewCell {
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
func setLabel() {
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
}
Here the additional necessary code to get the number of rows and return the cells to the TableView which I have in my ViewController. self.lastGamesCount is just an Int here and definitely not zero when I print it.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.lastGamesCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! GameCell
In my viewDidLoad() I register the cells like this:
tableView.register(GameCell.self, forCellReuseIdentifier: cellID)
When I run everything the Build is successful I can see the navigation bar of my App and all but the TableView is empty. I go back to the normal UITableViewCell and the cells are showing up again. What am I missing here? Any help is appreciated.
Thanks!
The problem is you need to set constraints for these labels
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
after you add them to the cell
class GameCell: UITableViewCell {
let mainTextLabel = UILabel()
let sideTextLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setLabel()
}
func setLabel() {
self.mainTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.sideTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(mainTextLabel)
self.contentView.addSubview(sideTextLabel)
NSLayoutConstraint.activate([
mainTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
mainTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
mainTextLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor,constant:20),
sideTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
sideTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
sideTextLabel.topAnchor.constraint(equalTo: self.mainTextLabel.bottomAnchor,constant:20),
sideTextLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor,constant:-20)
])
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Add/remove subview and constraints in UITableViewCell on button click

I have a UITableView that I'm using to show an array of custom objects. Each object has several properties including a Boolean property that indicates if this item is new or not.
My UITableViewCell content view is defined in the storyboard and has an initial layout similar to this:
In my UITableViewController, when I dequeue my cells, I call a method on my UITableViewCell that configures the data to be displayed in the cell before I return it. One of the properties that I check is the .isNew property that I mentioned previously. If this value is true, then I am creating a UIButton and inserting it as a subview in the cell's content view so I end up with something like this:
Just for context, this button will show a "new" image to indicate that this item is new. I am also hooking up a method that will fire when the button is tapped. That method is also defined in my UITableViewCell and looks like this:
#objc func newIndicatorButtonTapped(sender: UIButton!) {
// call delegate method and pass this cell as the argument
delegate?.newIndicatorButtonTapped(cell: self)
}
I have also created a protocol that defines a delegate method. My UITableViewController conforms to this and I see that code fire when I tap on the button in my cell(s). Here's is the delegate method (defined in an extension on my UITableViewController):
func newIndicatorButtonTapped(cell: UITableViewCell) {
if let indexPath = self.tableView.indexPath(for: cell) {
print(indexPath.row)
}
}
I see the row from the indexPath print out correctly in Xcode when I tap on the cell. When my user taps on this button, I need to remove it (the button) and update the constraint for my UILabel so that is aligned again with the leading edge of the content view as shown in the first mockup above. Unfortunately, I seem to be running into an issue with cell recycling because the UIButton is disappearing and re-appearing in different cells as I scroll through them. Do I need to reset the cell's layout/appearance before it gets recycled or am I misunderstanding something about how cell recycling works? Any tips would be much appreciated!
What you may be thinking is that you get a "fresh" cell, but when a cell gets re-cycled that means it gets re-used.
You can see this very easily by changing the text color of a basic cell.
For example:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyID", for: indexPath) as! MyCustomCell
if indexPath.row == 3 {
cell.theLabel.textColor = .red
}
return cell
}
As you would expect, when the table first loads the text color will change for the 4th row (row indexing is zero-based).
However, suppose you have 100 rows? As you scroll, the cells will be re-used ... and each time that original-4th-cell gets re-used, it will still have red text.
So, as you guessed, yes... you need to "reset" your cell to its original layout / content / colors / etc each time you want to use it:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyID", for: indexPath) as! MyCustomCell
if indexPath.row == 3 {
cell.theLabel.textColor = .red
} else {
cell.theLabel.textColor = .black
}
return cell
}
You may want to consider to have the button hidden and then change the layout when it is clicked.
Firing the action from the cell to the tableView with a protocol and then reseting the layout at cell reuse is a good way to do it
Doing it in a cell fully programatic would be like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
cell.isNew = indexPath.row == 0 ? true : false
cell.layoutIfNeeded()
return cell
}
And the cell class needs to be similar to: (you can do what you need by changing the Autolayout constraint, manipulating the frame directly or using a UIStackView)
class Cell: UITableViewCell {
var isNew: Bool = false {
didSet {
if isNew {
button.isHidden = true
leftConstraint.constant = 20
} else {
button.isHidden = false
leftConstraint.constant = 100
}
self.setNeedsLayout()
}
}
var button: UIButton!
var label: UILabel!
var leftConstraint: NSLayoutConstraint!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
button = UIButton(type: .system)
button.setTitle("Click", for: .normal)
self.contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 10).isActive = true
button.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 20).isActive = true
button.topAnchor.constraint(equalTo: self.topAnchor, constant: 20).isActive = true
label = UILabel()
label.text = "Label"
self.contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.widthAnchor.constraint(equalToConstant: 200).isActive = true
label.heightAnchor.constraint(equalToConstant: 20).isActive = true
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
leftConstraint = label.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 100)
leftConstraint.isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

UIView's not being added to contentView?

I'm trying to add some views in code to the contentView of a table view cell. However nothing shows up. I get an empty cell. Below is the code of me adding subviews to the content view of a custom cell.
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViews()
}
func setupViews() {
//feedback_title setup
feedback_title.text = Constants.IndividualStudiesPage.FEEDBACK_TITLE
feedback_title.numberOfLines = 0
feedback_title.lineBreakMode = .ByTruncatingTail
feedback_title.preferredMaxLayoutWidth = standard_height_width
//feedback_box setup
feedback_box.text = Constants.IndividualStudiesPage.DEFAULT_FEEDBACK
// TODO: Change to infinte (0) number of lines. Height constraint interferes with this
feedback_box.numberOfLines = 2
feedback_box.preferredMaxLayoutWidth = standard_height_width
contentView.addSubview(feedback_title)
contentView.addSubview(feedback_box)
}
Here's the interesting thing. When I add some dummy view like an empty UILabel to the content view in the storyboard, then everything shows up including the views I added in the code. Here's a picture to show you what I mean:
But when I don't add any dummy view then nothing shows up...Why and how can I fix this? Thanks in advance.
EDIT:
The feedback_cell and feedback_box are instantiated like so at the top of the class
class FeedbackCell: UITableViewCell {
var feedback_title : UILabel = UILabel.newAutoLayoutView()
var feedback_box : UILabel = UILabel.newAutoLayoutView()
newAutoLayoutView is a PureLayout function that essentially just instantiates the labels to empty views. I then update the AutoLayout constraints, also using PureLayout in the updateConstraints function:
override func updateConstraints() {
if !did_update_constraints {
//prevent labels from being compressed below intrinsic height but also from taking too much height
NSLayoutConstraint.autoSetPriority(UILayoutPriorityRequired){
self.feedback_title.autoSetContentHuggingPriorityForAxis(.Vertical)
self.feedback_title.autoSetContentCompressionResistancePriorityForAxis(.Vertical)
self.feedback_box.autoSetContentHuggingPriorityForAxis(.Vertical)
self.feedback_box.autoSetContentCompressionResistancePriorityForAxis(.Vertical)
}
//height constraints
feedback_title.autoSetDimension(.Height, toSize: standard_height_width)
//feedback_title constraints
feedback_title.autoPinEdgeToSuperviewMargin(.Leading)
feedback_title.autoPinEdgeToSuperviewMargin(.Trailing, relation: NSLayoutRelation.LessThanOrEqual)
feedback_title.autoPinEdgeToSuperviewMargin(.Top)
//feedback_box constraints
feedback_box.autoPinEdge(.Top, toEdge: .Bottom, ofView: feedback_title, withOffset: 10, relation: .GreaterThanOrEqual)
feedback_box.autoPinEdgeToSuperviewMargin(.Leading)
feedback_box.autoPinEdgeToSuperviewMargin(.Trailing, relation: NSLayoutRelation.LessThanOrEqual)
feedback_box.autoPinEdgeToSuperviewMargin(.Bottom)
did_update_constraints = true
}
super.updateConstraints()
View hierarchy debugger:
Tableview code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch (indexPath.row) {
case 0:
return setupDescriptionCell(tableView, indexPath: indexPath)
case 1:
return setupFeedbackCell(tableView, indexPath: indexPath)
case 2:
return setupStatsCell(tableView, indexPath: indexPath)
case 3:
return setupSurveyCell(tableView, indexPath: indexPath)
default:
return setupDefaultCell(tableView, indexPath: indexPath)
}
}
func setupFeedbackCell(tableView : UITableView, indexPath : NSIndexPath)->UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cell_IDs[1], forIndexPath: indexPath) as! FeedbackCell
//in the future download feedback in view did load and adjust here
return cell
}

swift - rendering table view is slow

I'm have a table view and I'm using the tableView.dequeueReusableCellWithIdentifier to reuse the cells but still tableView is very slow.
and by slow, I mean it takes about 500 milliseconds to put 9 of my views in the tableView. and it's tested on apple A7 X64 processor so it must be pretty slower on older processors.
the reason that it's slow is because there are a few sub views and constraints.
but I've seen more complex tableCells with better performance, so there must be something I can do.
like caching a cell or something else??
any ideas?
sample code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
tableView.registerNib(UINib(nibName: "ChatCell", bundle: nil), forCellReuseIdentifier: "ChatCell")
let cell = tableView.dequeueReusableCellWithIdentifier("ChatCell") as! ChatCell
return cell
}
the reason that it's slow is because there are a few sub views and constraints.
Personally, I don't suggest you use constraints in cell, especially when there're many subviews, it'll cost much CPU time and lead the scrolling lag. Instead, you can calculate manually based on cell frame.
And for more suggestion, i suggest you take time to read this post: Simple Strategies for Smooth Animation on the iPhone.
The call to registerNib is normally done only once in viewDidLoad, not every time you are asked for a cell in cellForRowAtIndexPath. Not sure how slow that call is, but it might be the reason for your slow response.
I think you are using effects (like shadow or round corners or etc) or having heavy calculations on UI
Edit: Code Sample added
//Add in your init func
tblView.registerClass(MSCustomVerticalListCell.self, forCellReuseIdentifier: NSStringFromClass(MSCustomVerticalListCell))
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCellWithIdentifier(NSStringFromClass(MSCustomVerticalListCell), forIndexPath: indexPath) as! MSCustomVerticalListCell
//add data binding
cell.item = dataSource[indexPath.row]
return cell
}
Your data binding class (Data Model):
class MSC_VCItem
{
var Title:String!
var Action:String!
var SubTitle:String!
var Icon:String!
init(title:String!,subTitle:String!,icon:String!,action:String!)
{
self.Title = title
self.SubTitle = subTitle
self.Icon = icon
self.Action = action
}
}
And Finally you custom table cell:
class MSCustomVerticalListCell : UITableViewCell {
let padding = 5
let imageWidth = 50
var customImageView: UIImageView!
var customTitleLabel: UILabel!
var customSubtitleLabel: UILabel!
var item: MSC_VCItem? {
didSet {
if let it = item {
customTitleLabel.text = it.Title
customSubtitleLabel.text = it.SubTitle
UIImage.loadFromCacheOrURL(it.Icon, callback: { (image: UIImage) -> () in
self.customImageView.image = image
})
setNeedsLayout()
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = UIColor.clearColor()
customTitleLabel = UILabel(frame: CGRectZero)
self.addSubview(customTitleLabel)
customSubtitleLabel = UILabel(frame: CGRectZero)
contentView.addSubview(customSubtitleLabel)
customImageView = UIImageView(frame: CGRectZero)
customImageView.image = UIImage(named: "default")
contentView.addSubview(customImageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Write your UI here like bg color or text color
}
}

Using auto layout in UITableviewCell

I am trying to use auto sizing UITableView cells in swift with snapKit! relevant flags set on the UITableView are as follows:
self.rowHeight = UITableViewAutomaticDimension
self.estimatedRowHeight = 70.0
I have a UITextField defined in my customUITableviewCell class like:
var uidTextField: UITextField = UITextField()
and the initial setup of the text field in my custom UITableViewCell looks like this:
self.contentView.addSubview(uidTextField)
uidTextField.attributedPlaceholder = NSAttributedString(string: "Woo Hoo", attributes: [NSForegroundColorAttributeName:UIColor.lightGrayColor()])
uidTextField.textAlignment = NSTextAlignment.Left
uidTextField.font = UIFont.systemFontOfSize(19)
uidTextField.returnKeyType = UIReturnKeyType.Done
uidTextField.autocorrectionType = UITextAutocorrectionType.No
uidTextField.delegate = self
uidTextField.addTarget(self, action: "uidFieldChanged", forControlEvents: UIControlEvents.EditingChanged)
uidTextField.snp_makeConstraints { make in
make.left.equalTo(self.contentView).offset(10)
make.right.equalTo(self.contentView)
make.top.equalTo(self.contentView).offset(10)
make.bottom.equalTo(self.contentView).offset(10)
}
when I run the code it shows up cut off and gives me an error in the console that reads:
Warning once only: Detected a case where constraints ambiguously
suggest a height of zero for a tableview cell's content view. We're
considering the collapse unintentional and using standard height
instead.
Is there something wrong with my autoLayout constraints or is this an issue with UIControls and autosizing of UITableView cells?
In SnapKit (and Masonry) you have to use negative values to add a padding to the right or bottom of a view. You are using offset(10) on your bottom constraint which causes the effect that the bottom 10pt of your text field will get cut off.
To fix this you have to give your bottom constraint a negative offset:
uidTextField.snp_makeConstraints { make in
make.left.equalTo(self.contentView).offset(10)
make.right.equalTo(self.contentView)
make.top.equalTo(self.contentView).offset(10)
make.bottom.equalTo(self.contentView).offset(-10)
}
Or you could get the same constraints by doing this:
uidTextField.snp_makeConstraints { make in
make.edges.equalTo(contentView).inset(UIEdgeInsetsMake(10, 10, 10, 0))
}
When you are using the inset() way you have to use positive values for right and bottom inset.
I don't understand why SnapKit uses negative values for bottom and right. I think that's counterintuitive and a bit confusing.
EDIT: This is a little example that is working fine (I hardcoded a tableView with 3 custom cells that include a UITextField):
ViewController:
import UIKit
import SnapKit
class ViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.dataSource = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 70
tableView.registerClass(CustomTableViewCell.self, forCellReuseIdentifier: "CustomCell")
tableView.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(view)
}
}
}
extension ViewController: UITableViewDataSource {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
return cell
}
}
CustomTableViewCell:
import UIKit
class CustomTableViewCell: UITableViewCell {
let textField = UITextField()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
textField.attributedPlaceholder = NSAttributedString(string: "Woo Hoo", attributes: [NSForegroundColorAttributeName:UIColor.lightGrayColor()])
textField.textAlignment = NSTextAlignment.Left
textField.font = UIFont.systemFontOfSize(19)
textField.returnKeyType = UIReturnKeyType.Done
textField.autocorrectionType = UITextAutocorrectionType.No
contentView.addSubview(textField)
textField.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(contentView).inset(UIEdgeInsetsMake(10, 10, 10, 0))
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Resources