UITableViewCell dynamic cell heights are incorrect until scrolled - ios

I want my UITableViewCell to expand in size when tapped.
The layout of the cell is quite straightforward. Within the UITableViewCell is a UILabel. The UILabel is constrained to the UITableViewCell with top, bottom, left and right anchors.
I also have two stored properties. labelExpandedHeightConstraint stores the UILabel's height constraint for when the label is expanded. labelCompactHeightConstraint stores the UILabel's height constraint for when the label is compacted. Notice that labelCompactHeightConstraint is initially set to active.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
let spacing = 8
self.addSubview(self.labelView)
self.labelView.translatesAutoresizingMaskIntoConstraints = false
self.labelView.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
self.labelView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -1 * spacing).isActive = true
self.labelView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
self.labelView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -1 * spacing).isActive = true
self.labelExpandedHeightConstraint = self.labelView.heightAnchor.constraint(equalToConstant: 120)
self.labelCompactHeightConstraint = self.labelView.heightAnchor.constraint(equalToConstant: 80)
self.labelCompactHeightConstraint.isActive = true
}
The expand() function below is called whenever the user taps a UITapGestureRecognizer. This function is very simple. It expands the cell by disabling labelCompactHeightConstraint and enabling labelExpandedHeightConstraint.
#objc func expand() {
self.labelCompactHeightConstraint.isActive = false
self.labelExpandedHeightConstraint.isActive = true
self.layoutIfNeeded()
}
The problem is that when the expand() function is called, the UITableViewCell and its contents do not change in size. It is not until the user scrolls the cell off the screen, and then scrolls it back onto the screen, that the size adjusts correctly.
How can I get the cell to expand immediately when tapped? I would also like this sizing change to be animated. I would really appreciate any suggestions. Thanks!

You'll need to do it differently.
Something like:
tableView.beginUpdates()
// update data for cell, or if your cell is not dynamically created - update it directly
// Usually, you'll need to update your data structures
// Reload the cell
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
From what you wrote, the place to add this code is from:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
}
Also, note that in most cases, you should just change the content (ie. the text in the label) and not the constraint value.
Here is a minimal full example:
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ident", for: indexPath) as! Cell
if selections.contains(indexPath) {
cell.height.constant = 80
} else {
cell.height.constant = 10
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.beginUpdates()
if selections.contains(indexPath){
selections.remove(indexPath)
} else {
selections.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
var selections = Set<IndexPath>()
}
class Cell : UITableViewCell {
#IBOutlet var height : NSLayoutConstraint!
}
Could use the table view's multiple selection but wanted to demonstrate usage of app specific data.

Related

How to narrow UITableView cell height if there is dynamic structure in Swift?

I have a tableView and cells. The Cells are loaded from a xib and they have a label with automatic height. I need to narrow one cell if the user taps on it.
I have tried hiding - doesn't work
I have tried removeFromSuperView()- doesn't work
Is there any alternative?
When setting up your tableViewCell store the height anchor you want to update
var yourLabelHeightAnchor: NSLayoutConstraint?
private func setupLayout() {
yourLabelHeightAnchor = yourLabel.heightAnchor.constraint(equalToConstant: 50)
// Deactivate your height anchor as you want first the content to determine the height
yourLabelHeightAnchor?.isActive = false
}
When the user clicks on a cell, notify the tableView that the cell is going to change, and activate the height anchor of your cell.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCellIdentifier") as? YourCell
self.tableView.beginUpdates()
cell?.yourLabelHeightAnchor?.isActive = true
self.tableView.endUpdates()
}
Did you try to do something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var result: CGFloat
if (indexPath.row==0) {
result = 50 }
else {result = 130}
return result
}
This is just an example where height is changed for the first row. I tested on my application and it gave result like this.

Self sizing cells height change causes jumping animation

I have setup a tableview with dynamic height cells aka UITableView.automaticDimension using Autolayout. This works fine. Now what I am trying to achieve is to change the height of cell & animate it. The issue is that when I change cell height & animate it, the animation is weirdly jumping. The jump only occurs if I scroll down a bit & then expand/collapse cells.
I have a simple table view cell. It has a label & an empty UIView with fixed height constraint. When I want to collapse/expand the cell, I simply change the constant of that height constraint to 0 or 300.
I have tried many collapsable tableview examples off the internet. All of them have this issue. One exception is https://github.com/Ramotion/folding-cell, but that uses fixed heights for cells.
I have tried quite a few options to animate the cell height change.
1-> On didSelectRow, I change the height constraint & call tableview beginUpdate & endUpdates. Doesn't solve the jump issue.
2-> Change my model & call tableView.reloadRows. Doesn't solve the jump issue.
This is screenshot of my tableview cell setup.
https://drive.google.com/open?id=12nba6cwRszxRlaSA-IhrX3X_vLZ4AWxy
A link to video of this issue:
https://drive.google.com/open?id=19Xmc0PMXT0EuHTJeeGHm4M5aPoChAtf3
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! OuterTableViewCell
let height: CGFloat = isCellExpanded[indexPath.row] ? 300 : 0
cell.labelText.text = "Cell Number: \(indexPath.row + 1)"
cell.buttonExpansionToggle.setImage(UIImage(named: isCellExpanded[indexPath.row] ? "arrow-down" : "arrow-right"),
for: .normal)
cell.viewContainerHeight.constant = height
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isCellExpanded[indexPath.row] = !isCellExpanded[indexPath.row]
tableView.reloadRows(at: [indexPath], with: .automatic)
}
Another form of didSelectRow:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? OuterTableViewCell else { return }
isCellExpanded[indexPath.row] = !isCellExpanded[indexPath.row]
let height: CGFloat = self.isCellExpanded[indexPath.row] ? 300 : 0
cell.viewContainerHeight.constant = height
cell.layoutIfNeeded()
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
tableView.beginUpdates()
tableView.endUpdates()
// fix https://github.com/Ramotion/folding-cell/issues/169
if cell.frame.maxY > tableView.frame.maxY {
tableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.bottom, animated: true)
}
}, completion: nil)
}
I have also tried to call beginUpdates() & endUpdates() outside animation block, yet the issue persists.
I expect the animation to be smooth. Hope someone can help. If someone can setup a simple demo project on github that would be awesome.
Demo project link: https://gitlab.com/FahadMasoodP/nested-tableviews
Help in any form is appreciated. Thanks.
My solution is adding dynamic estimate row height. I think it is bug of UIKit. iOS 13 issue will be not occur.
First, You need add property estimateRowHeightStorage to store estimate height by indexpath
var estimateRowHeightStorage: [IndexPath:CGFloat] = [:]
Second, You need store current height of cell for use later
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
estimateRowHeightStorage[indexPath] = cell.frame.size.height
}
Final, you use estimateRowHeightStorage to set estimate row height.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if estimateRowHeightStorage[indexPath] != nil {
return estimateRowHeightStorage[indexPath]!
}
return 50
}
Run and feel.
I did found new solution in your case. If you hardfix height when expand. Only need change some thing.
Replace all code above with estimatedHeightForRowAt function
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let height: CGFloat = isCellExpanded[indexPath.row] ? 373 : 73
return height
}

UITableView dequeueReusableCell duplicates constraints

Using swift, I have a UITableView and custom UITableViewCells. After each cell is initialized I call a method ( layoutViews() ) to apply the appropriate constraints ( NSLayoutConstraint.activate([...] ). One constraint, a labels leading anchor, is dynamic based on the row and results in somewhat of an indented look between parent and child records.
Example :
Parent
Child
Parent
Parent
Child
Grandchild
Child
Child
As I scroll the constraints get confused and result in duplicate constraints on the same cell
Error: Unable to simultaneously satisfy constraints... Will attempt to recover by breaking constraint).
Most of the time no indentation is performed when it should be.
I have tried to deactivate the previous constraints, remove previous constraints, and remove all views from the superview. I got various results, but none were correct.
I just want the indentation to match the CGFloat value I am providing in the getIndentaion() function.
As requested, here is the relevant code (abbreviated form, of course).
In the UIViewController:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.setIndentation(2)
cell.layoutViews()
return cell
}
In the CustomCell:
func getIndentation() -> CGFloat {
return 5 + ((indentation ?? 0) * 15)
}
func layoutViews() {
NSLayoutConstraint.activate([
messageLabel.leadingAnchor.constraint(equalTo: messageBackground.leadingAnchor, constant: getIndentation())
])
}
Any help is appreciated.
Thank you, #Paulw11. The following solution worked:
In the UIViewController:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.setIndentation(2)
return cell
}
In the CustomCell:
var indentationConstraint: NSLayoutConstraint!
func layoutViews() {
indentationConstraint = messageLabel.leadingAnchor.constraint(equalTo: messageBackground.leadingAnchor, constant: 5)
NSLayoutConstraint.activate([
indentationConstraint
])
}
}
func setIndentation(_ indentation: CGFloat) {
let totalIndentation: CGFloat = 5 + (indentation * 15)
indentationConstraint.constant = totalIndentation
messageLabel.layoutIfNeeded()
}

tableView does not appear when centered into its parent view

I have a viewController with the following (static) tableView:
class viewController: UIViewController, UITableViewDelegate {
private let tableView: UITableView = {
let tv = UITableView()
tv.separatorStyle = .singleLine
tv.allowsSelection = true
tv.isScrollEnabled = false
return tv
}()
private let tableData = ["row1", "row2", "row3", "row4"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.tableFooterView = UIView()
view.addSubview(tableView)
NSLayoutConstraints.activate([
tableView.centerXAnchor.constraints(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
tableView.centerYAnchor.constraints(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
)]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
cell.textLabel!.text = tableData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
}
When I run the app, this viewController shows a blank screen. I know that the way I am setting up the tableview's constraints is the problem because when I set up the tableView using topAnchor, bottomAnchor, leftAnchor, and rightAnchor (and with some other tweaking) the tableview appears. Any idea why the app is behaving this way?
Your table view is probably there, and centered, but you didn't define a size, so it's probably being set to zero width and height, that's why you don't see it.
You can fix this by setting a constraint on it's width and height, either to a constant or related to it's superview, depending on what you want.
The problem is this is NOT a static table view. If it were, you would not have implemented cellForRowAt. It is a normal table view and it needs a data source and delegate. Plus it needs a height and a width.

Swift What is the best way to replace content in TableViewCell

I want to do something like in the GIF
I tried 2 ways, one was hiding the elements on selecting the row and showing others, but that's not really elegant and doesn't work very well
and second was creating 2 views, one with labels, another with buttons, adding them as subviews to cell.contentView but that caused some issues with other cells as they were displaying wrong data. How can I recreate something like this?
I think something like this would work:
Use 2 different UITableViewCells: add them to the table view in your storyboard and design them separately, also you can use 2 different UITableViewCell subclasses for them
Have an array in the tableview's datasource class that will define the type of the cell from each row (e.g. the simplest solution would be an array of integers, with values: 0 representing the first cell, 1 representing the second cell)
Initialise that array with 0s for each row
In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell :
if cellTypes[indexPath.row] == 0 --> return a cell of first type
if cellTypes[indexPath.row] == 1 --> return a cell of the second type
In tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) :
switch the cell type in the array
reload the row with animation, e.g. you can use .fade or .left or .right etc.
tableView.reloadRows(at: [indexPath], with: .fade)
EDIT: Your solution is also a good one, but it can cause problems when the cells are dequeued, so if a cell with the wrong subviews is dequeued then you need to switch the subviews back in the cellForRowAt indexPath datasource method.
EDIT2: I took some time and I have tried my solution in Xcode. Here is the code of my tableview controller:
class TableViewController: UITableViewController {
private var cellTypes: [Int] = [1, 1, 1, 1, 1, 1]
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.cellTypes.count
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 56.0
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.cellTypes[indexPath.row] == 1 {
return tableView.dequeueReusableCell(withIdentifier: "Cell1", for: indexPath)[![enter image description here][1]][1]
} else {
return tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath)
}
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if self.cellTypes[indexPath.row] == 1 {
self.cellTypes[indexPath.row] = 2
tableView.reloadRows(at: [indexPath], with: .fade)
} else {
self.cellTypes[indexPath.row] = 1
tableView.reloadRows(at: [indexPath], with: .right)
}
}
}
And here is how it is working in the iOS simulator:
I believe you are on the right track about creating 2 separate views inside the cell; one for showing 3 buttons (Play Now, Play Next etc.) and, one for showing the song's details (song name, singer name etc.).
In order not to mess with frames or constraints (in case you are using Autolayout), the main trick here is to create a snapshot of the view containing the buttons and move it to the end of the cell.
As I said above, you should have 2 separate views. I'll call them:
infoView: View that has 2 labels showing the song's and the singer's name.
actionsView: View that has 3 buttons for play actions. (Now, Next, Last etc.)
Here are things that you should do when user taps on a cell:
Check if cell is not selected. If it is not, then hide infoView and show actionView.
If cell is selected:
Deselect the cell.
Create a snapshot out of actionsView, set its frame accordingly so it'll shadow the real actionsView.
Set actionView's isHidden property to true.
Set infoView's isHidden property to false.
Set frame.origin.x value of the snapshot to contentView's maxX in an animation block so it'll move to the right side of the cell smoothly.
At the end of the animation, remove the snapshot from view hierarchy.
I've created a cell class and defined a method that executes those steps:
public class SongCell: UITableViewCell {
#IBOutlet fileprivate weak var infoView: UIView!
#IBOutlet fileprivate weak var actionsView: UIView!
...
public func showActions(_ show: Bool) {
switch show {
case true:
infoView.isHidden = true
actionsView.isHidden = false
case false:
if let snapshot = actionsView.snapshotView(afterScreenUpdates: true) {
snapshot.frame = actionsView.frame
contentView.addSubview(snapshot)
actionsView.isHidden = true
infoView.isHidden = false
UIView.animate(withDuration: 0.25, animations: {
snapshot.frame.origin.x = self.contentView.frame.maxX
}, completion: { _ in
snapshot.removeFromSuperview()
})
}
else {
infoView.isHidden = false
actionsView.isHidden = true
}
}
}
}
Here is how it looks on my simulator:
You can download the project from here.

Resources