I need to update a constraint's value inside a UITableViewCell, which should trigger resizing of the cell. I need to know the correct frame before I can update the constraint's value, however having my code inside layoutSubviews() does not resize the cell. The subview which has the constraint attached to it grows, but cell stays the same initial height, cutting off the view.
My setup for the purpose of demonstration is very simple, I have a single UIView pinned to the edges of the cell's contentView, and a height constraint added to it. I expect the cell to resize when the height constraint of the view is changed.
override func layoutSubviews() {
super.layoutSubviews()
heightConstraint.constant = 500
}
Any ideas on why this is not working?
There are a couple quirks with changing cell height in layoutSubviews().
Typically, when a cell modifies its internal layout (constraints, etc), the cell must tell the table view to redraw itself.
In your case, the cell's layoutSubviews() is being called after the table view has determined the cell's height.
Couple ways around this, if you really do need to do that in layoutSubviews():
1 - call relaodData() in viewDidAppear()
2 - force the cell layout inside cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
// set your cell properties here as usual - labels, images, etc...
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell
}
I have such problem, while trying to create a simple messenger.
Here what I came to:
class ChatBubleUIView that class is responsible for creating a bubleview with label in it. It works fine, calculating view height according to label height
Inside my cell I've created a content view. In the cell class, I'm adding new ChatBubleUIView instance as a subview to content View.
The problem is that, content doesn't scale up to the size of my ChatBubleInstance.
class ChatMessageTableViewCell: UITableViewCell, MessageCellConfiguration {
var message: Message?
override func awakeFromNib() {
super.awakeFromNib()
}
func configureCell() {
let chatBubleView = ChatBubleUIView(message: message!)
self.addSubview(chatBubleView)
}
}
In my tableView delegate
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "messageCell") as! ChatMessageTableViewCell
let data = currentUser.mesaageHistory[indexPath.row]
cell.message = data
cell.configureCell()
return cell
}
Also I have set estimated row height for my tableView
messageTableView.rowHeight = UITableViewAutomaticDimension
messageTableView.estimatedRowHeight = 44
What should I do to set to my tableView row height chatViewBubleUIView instance height.
Previously, I solved this problem using old-school approach, programmaticly determine chatViewBubleUIView instance height and then implement heightForRowAtIndexPath. But I'd like to do that using AutoLayoaut.
set your label's four constraint like top,bottom,leading,trailing and number of height of your label should be 0. Then it will automatically increased it's height as per content. If you are taking this label in any view then view's constrains should be same as label i have mentioned above!
Senario A:
If I set the label content in cellForRowAtIndexPath, the cell correctly get resized.
Senario B:
If I change the text content in custom action in cell, the cell sized does not get changed.(I do call setNeedsLayout + layoutIfNeeded)
How to fix this?
EDIT:
1) I have set,
myTableView.estimatedRowHeight = 71.0
myTableView.rowHeight = UITableViewAutomaticDimension
2) I have correctly added auto layout constraints.
I was running into this issue, and my problem was that I was constraining the content to self (the UITableViewCell) and not to self.contentView (the contentView OF the cell). Hope this helps someone else who has built their cells all in code!
In my case, the cell's custom size was enabled:
After you change the text of the cell, just reload that particular cell or simply call mainTableView.reloadData().
To reload that cell-
//indexPath is indexPath of cell you just changed label of
mainTableView.reloadRows(at: indexPath, with: .automatic)
In my case, in the same cell I had an imageView in the top left corner with a "center vertically in container" constraint, and a "top space container" constraint.
Obviously to satisfy this two constraint the cell must have an height equal to:
(height of the imageView / 2) + (length of the top space container constraint).
This height is not enough to fit the label text, so my label had only 1 line visible.
After I have deleted the imageView top constraint all went to the right place, in my case i wanted the image to be centered, if the image had to stay in the top left corner I had to take off the "center vertically in container" constraint.
I hope this can help someone.
First of all, I don't specifically know what was your action on UITableViewCell. So, I assume I do that in UITableViewCell selection.
The below answer only work on iOS 9 and above
But, for some reason, it failed to do it in iOS 8 until it scroll. So, I will update the answer for iOS 8.
I have seen you have used UITableView's estimatedRowHeight and rowHeight at your project. So,
Please check the following
Make sure UITableView's estimatedRowHeight and rowHeight include inside viewDidLoad()
Make sure your UILabel lines set to 0
Make sure there is no constraints about height for your UILabel and let the constraints be like that :
If there are other component also included, make sure only bottom and top space constraints included or top space to container margin and bottom space to container margin.
Every time that you want to update the cell, you have to reload tableView no matter what your current situation will be.
So, don't say anything yet before you try this sample project, #Rikh answer still work. May be you are going in wrong direction. Here's the solution. Please do as I said steps by steps and let me know if that didn't work out. You might need to share your sample project which is causing.
Sample Demo - DynamicCellDemo
UPDATE for iOS 8 : update the following code for iOS 8 users
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 9, *) {
// do nothing
} else {
tblDynamic.reloadData()
}
}
what you can do is set the AutoLayout constraints for the label in present in the cell from all the sides that is from Top, Bottom, Leading and Trailing. Then add the following UITableViewDelegate method to your class.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 500
}
This will do the job. As now content in table view cell automatically adjusts the height of the cell.
Try this if it works:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Add the following in your viewDidLoad()
-(void)viewDidLoad{
[super viewDidLoad];
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
}
I have a subclass of UITableViewCell and a subclass of UITableViewController. I'm popualating the tableview with my custom cells.
I created a UITableViewCell in my storyboard, made its class my custom UITableViewCell Swift file, and gave it an identifier. Then I create cells in the controller with:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myIdentifier", forIndexPath: indexPath indexPath)
return cell
}
In my custom cell (on the storyboard) I have a UIImageView with the following constraints:
Image View Constraints
The constant for the top space constraint is 0, which I thought means the ImageView top would be flush with the cell top.
However, the images in my cell are pushed down a bit:
UITableView with custom cell
The white is just the table section header, that's working perfectly. However, you can see the orange gap that appears above the image (that orange is the background color of the cell), and I don't want that there.
I tried setting the image frame in the custom cell's awakeFromNib:
#IBOutlet weak var img: UIImageView!
override func awakeFromNib() {
img.frame = CGRectMake(0, 0, img.frame.width, img.frame.height)
}
But this had no effect, so I tried setting the frame in layoutSubviews:
override func layoutSubviews() {
img.frame = CGRectMake(0, 0, img.frame.width, img.frame.height)
}
This worked, but only after scrolling down and back up on the TableView. The first two cells have the orange gap, but if you scroll to the third one and back up, then the gap is gone.
Does anyone know why this is happening? How can I make the imageview be positioned at (0, 0) right away instead of only after scrolling down and back up?
When I try to reproduce your situation and drag the imageView to align to the top of the cell the Xcode suggested constraint is -8, and it works fine like that but if i set it to 0 I get the background color like your problem, try setting the top constraint to -8 or whatever Xcode is suggesting.
I'm not very familiar with AutoLayout so I can't clarify why Xcode wants to set it to -8
Try to set the height of tableViewCell through the following code:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UIScreen.mainScreen().bounds.width
}
I have a grouped UITableview which is created programatically. Also I have a cell with xib file populated in tableview programmatically as well. So far so good. But I want to only remove outer separator line. I used below code but this time removed all separator line.
self.tableView.separatorColor = [UIColor clearColor];
this is not good option for my situation. Here is the screenshot what i want to do;
You can remove separators even in grouped UITableView with Static Cell:
class CustomCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
for view in subviews where view != contentView {
view.removeFromSuperview()
}
}
Here's my solution:
self.tableView.separatorColor = self.tableView.backgroundColor
Google, even in 2018, is serving this page as the top result for this question. I didn't have any luck in iOS 11 with any of the provided answers, so here's what I came up with:
extension UITableViewCell {
func removeSectionSeparators() {
for subview in subviews {
if subview != contentView && subview.frame.width == frame.width {
subview.removeFromSuperview()
}
}
}
}
Calling .removeSectionSeparators() on any UITableViewCell instance will now take care of the problem. In my case at least, the section separators are the only ones with the same width as the cell itself (as the other ones are all indented).
The only question left is from where we should call it. You'd think willDisplayCell would be the best choice, but I discovered that the initial call occurs before the separator views themselves are generated, so no dice.
I ended up putting this in my cellForRowAtIndexPath method just before I return a reloaded cell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyReusableIdentifier", for: indexPath)
Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { (timer) in
cell.removeSectionSeparators()
}
return cell
}
It doesn't feel that elegant, but I haven't run into any issues yet.
EDIT: Looks like we need this too (for reused cells):
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.removeSectionSeparators()
}
Here's a before/after in screenshots with this code:
Before
After
I just worked out a solution, as the cell has contentView which is a UIView, so I think you can just focus on the bottomline of contentView.
Here is my code:
first, you have to make the separator to clear
tableView.separatorColor = UIColor.clear
Second, in the cellForRowAt function:
let bottomBorder = CALayer()
bottomBorder.frame = CGRect(x: 0.0, y: 43.0, width: cell.contentView.frame.size.width, height: 1.0)
bottomBorder.backgroundColor = UIColor(white: 0.8, alpha: 1.0).cgColor
cell.contentView.layer.addSublayer(bottomBorder)
here you will see the UI like this:
For removing the top and bottom part of separator line for each section. Add this to your static cell.
override func layoutSubviews() {
super.layoutSubviews()
//Get the width of tableview
let width = subviews[0].frame.width
for view in subviews where view != contentView {
//for top and bottom separator will be same width with the tableview width
//so we check at here and remove accordingly
if view.frame.width == width {
view.removeFromSuperview()
}
}
}
Result as below image
After inspecting the view hierarchy, it seems each UITableViewCell has only three subviews: the content view (UITableViewCellContentView), and two separator views (_UITableViewCellSeparatorView). I'm not a fan of dynamic class instantiation from NSStrings (and neither is Apple 😉). However, because the contentView of a UITableViewCell is accessible without using private APIs, the solution turns out to be pretty easy.
The idea is just to iterate through the subviews of your UITableViewCell, and remove any views that aren't the contentView:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let subviews = cell.subviews
if subviews.count >= 3 {
for subview in subviews {
if subview != cell.contentView {
subview.removeFromSuperview()
break
}
}
}
}
tableView(:willDisplayCell:forRowAtIndexPath:) is called multiple times during table view rendering, so the above keeps track of state by checking how many subviews the cell has. If it has three subviews, both separators are still intact. It also only removes one of the separators, but you can remove both by removing the break. You can also specify whether to remove the top separator or the bottom separator by checking the subview's frame. If the frame's y-axis origin is 0, that's the top separator. If it's not 0, it's the bottom.
Hope this helps!
Swift 4, Swift 4.2 Update:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let subviews = cell.subviews
guard subviews.count >= 3 else {
return
}
for subview in subviews where NSStringFromClass(subview.classForCoder) == "_UITableViewCellSeparatorView" {
subview.removeFromSuperview()
}
}
iOS 14, Swift 5
In the custom cell class:
override func layoutSubviews(){
super.layoutSubviews()
for subview in subviews where (subview != contentView && abs(subview.frame.width - frame.width) <= 0.1 && subview.frame.height < 2) {
subview.removeFromSuperview() //option #1 -- remove the line completely
//subview.frame = subview.frame.insetBy(dx: 16, dy: 0) //option #2 -- modify the length
}
}
I want to thank #cook for this solution, as I built on top of it. I had some issues with their solution:
it was removing the default highlight/selected background view, so I added an extra check on the subview's height.
I put my code in layoutSubviews() and haven't had a single issue.
I implemented an approximation between two CGFloats instead of using the equality operator ==, which sounds error-prone to me.
I added the "option #2" in the code, as that's the solution I was personally looking for (I wanted to maintain the separator, but I wanted it to be at the same indentation level as the regular cell separators, in my case a value of 16).
That is a really old question, still it's one of the first entries on google when searching for how to remove the top and bottom separators for each section.
After some investigation, I found out that there is no way and just no intention from Apple to make this somehow happen without stupidly complicated hacks of the view hierarchy.
Fortunately there is a absolutely simple and easy way of achieving such a look:
I say simple but for a beginner, this might be difficult because of a lack of understanding how UITableView works and how to implement your own cells. Let me try to explain it:
Create a UITableViewCell subclass
In your cell, create a #IBOutlet weak var separatorView: UIView! property
In your Storyboard, select the tableview cell and select your own cell as the class which backs up the cell. Note: You do not have to use custom style. You still can use Basic, Subtitle, etc.
Set your UITableViews Separator Style to None to hide the default separator
Drag a UIView onto your cell and resize it so it is on the bottom (or the top) of the cell. Use the Size Inspectors Autoresizing to pin it to start/end/bottom and give it a flex width (or Autolayout but thats just over the top in this case)
Connect the view to the outlet of your cell class
In your UITableViewDataSources cellForRowAtIndexPath: set the isHidden property of your custom separator based on if the indexPath.row is the last row (or the first, if your view is at the top) in the section
Heres some example code:
class MyCell: UITableViewCell {
#IBOutlet weak var separatorView: UIView!
}
class ViewController: UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
cell.separatorView.isHidden = indexPath.row == 2
return cell
}
}
And here some screenshots:
Yes, it is some work but there is just no way to something for free when coding. In the end, we are programmers and it is up to us to do the coding. It's always better to spend the 5-10 minutes setting this up than just copy pasting some hacky code which might not continue to work in the future when Apple decides to change the view hierarchy.
It was actually more work to write this answer than implementing the separator. I as well was searching for a easy and quick solution but in the end there just wasn't a good enough one which I felt was worth using.
I hope you also feel skeptical when you see for-loops iterating over subviews of cells to hide or even remove views at runtime from the view hierarchy Apple provides you, when there is an easy, versatile, stable and future proof solution right around the corner. 7 little steps is really all you need and they are easy to understand.
This is what I finally came up with:
I was not able to find some better way than this. But it is working for me greatly. Last tried with Xcode Version 7.3.1 (7D1014). This procedure was done through storyboard.
Basically I add a UIView of 0.5 pt Height on the UITableViewCell and then set a background color for that UIView. Set parent UITableView's Separator as None.
Here is the details:
Considering you already set your UITableViewCell, custom or default. On the very first stage set the UITableView's separator as None.
Next add a UIView of 1 pt Height and set the Background as you need, on my case it is Red.
Start setting the constraints. The problem is to set the height of the UIView as 0.5 pt. This is the only Problematic issue for this workflow.
UIView with 0.5 pt Height:
Sharing the way to set 0.5 pt height of the UIView.
First(1) pin the view and then(2) set the height as 0.5. Press Enter.
Finally your UIView will look similar like following.
I was not able to set the height as 0.5 other than this way.
Here the solution. This is for static cells. If you want dynamic then just rewrite "count". Hope it helps.
extension NSObject {
var theClassName: String {
return NSStringFromClass(self.dynamicType).componentsSeparatedByString(".").last!
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorStyle = .None
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let count = tableView.numberOfRowsInSection(indexPath.section)
if ( indexPath.row != count - 1 ) {
for view in cell.subviews {
if view.theClassName == "_UITableViewCellSeparatorView" {
view.backgroundColor = UIColors.redColor()
}
}
}
}
Tried various solutions based on the cell.separatorInset = UIEdgeInsetsMake() workaround, none of them working properly.
For my iOS11 UITableViewStyleGrouped based project, this did it:
self.tableView.separatorColor = self.tableView.backgroundColor;
Based on cook's answer, but without a timer:
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if NSStringFromClass(subview.classForCoder) != "UITableViewCellContentView" && subview.frame.width == frame.width {
subview.removeFromSuperview()
}
}
Just add this in a cell's subclass and you don't need anything else. Works on iOS 12.
I had a similar issue, where I had a Grouped UITableView with custom cells, all designed with Interface Build as .xib files. The cells had white backgrounds, removed the default separators, and added my own ones. I also had custom Header Views for each section. I set the height of those Header Views to be 44 (could be anything...). There was a 1 point height view between my sections, which seemed weird. Apparently, the system adds some clear background view between the sections, even if we specify the custom height to be, say 44, and the custom Header View we return has a white (or some concrete background color). The color we see behind that clear view is the color of of the Table View's background actually. In my case, both the table views and the cells had to be of white color, and setting the background of table view to be white solved the problem (at least visually, but that's what I wanted anyway). Second solution would be to keep the Table View's style as plain, but implement the UITableView delegate method to return 2 or more sections, and also create custom headers if you need to. But that will make header views to stick to the top for a while (while scrolling), until the next section's header view gets closer to it, and then it starts going up too (and that may not be what you really want, but there may be an easy way to fix that, not sure though).
For a single-cell section, simply overriding layoutSubviews and leaving it empty does the trick! https://stackoverflow.com/a/59733571/4442627
Here's a Swift solution that works on iOS 8 and 9.
Define a protocol with a default implementation:
protocol CellSeparatorRemovable { }
extension CellSeparatorRemovable {
func removeSeparatorLinesFromCell(cell: UITableViewCell, section: Int, row: Int, indexPath: NSIndexPath) {
guard (section, row) == (indexPath.section, indexPath.row) else { return }
for view in cell.subviews where view != cell.contentView {
view.removeFromSuperview()
}
}
}
Then, wherever you want to use it, conform to the CellSeparatorRemovable protocol and call its method from …willDisplayCell…:
class SomeVC: UITableViewController, CellSeparatorRemovable {
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
removeSeparatorLinesFromCell(cell, section: 1, row: 1, indexPath: indexPath)
}
}
This is a minimal solution; you may need to refactor it if you're dealing with many cells to avoid excessive recursion and/or cell reuse issues.
You can access the view using
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header = view as! UITableViewHeaderFooterView
header.backgroundView . .....
Try this it will remove top separator line.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0 && indexPath.section == 0) {
for (UIView *view in cell.subviews) {
if ([NSStringFromClass([view class]) containsString:#"CellSeparator"]) {
if (view.frame.origin.y == 0 && CGRectGetHeight(view.frame) < 1.01 ) { //Hide first UITableViewCellSeparatorView
NSLog(#"%s Remove View From Cell[Section:0 Row:0] for top border [%#]:%#",__FUNCTION__,NSStringFromClass([view class]),view);
view.hidden = YES; //Hide
}
}
}
}
}
iOS 10~13 only remove section head foot line.
-(void)layoutSubviews{
[super layoutSubviews];
//for iOS10~iOS13: only remove section head foot line
for (UIView * v in self.subviews) {
if ( v != self.contentView &&
(v.frame.size.width == self.frame.size.width)){
[v removeFromSuperview];
}
}
}
if wan to remove all line:
for (UIView * v in self.subviews) {
if (v != self.contentView){
[v removeFromSuperview];
}
}
override func layoutSubviews() {
super.layoutSubviews()
subviews.filter { $0 != contentView && $0.frame.width == frame.width }.first?.removeFromSuperview()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = groupTable.dequeueReusableCell(withIdentifier: "groupCell", for: indexPath) as! GroupNameCell
if indexPath.row == 2 /*user your array.count-1 (I have not used array soused 0 here)*/ {
cell.separatorInset = UIEdgeInsets(top: .zero, left: .zero, bottom: .zero, right: .greatestFiniteMagnitude)
}
return cell
}