How to update height Constraints by SnapKit and update cell height? - ios

I have a TableViewCell and two button to switch different constrain.
I want to update it's height constrain and cell height.
like following pic1
when I click buttonB, the view will change like pic2
Then I click buttonA, the view will back to pic1
I try to modify constrains, but I fail to update height.
Have any idea or answer to me?
Thanks
pic1
pic2
Here is code:
class CellContainView: UIView {
let buttonA: UIButton = { () -> UIButton in
let ui = UIButton()
ui.titleLabel?.numberOfLines = 0
ui.setTitle("Click\nA", for: .normal)
ui.backgroundColor = UIColor.blue
return ui
}()
let buttonB: UIButton = { () -> UIButton in
let ui = UIButton()
ui.titleLabel?.numberOfLines = 0
ui.setTitle("Click\nB", for: .normal)
ui.backgroundColor = UIColor.gray
return ui
}()
let buttonC: UIButton = { () -> UIButton in
let ui = UIButton()
ui.titleLabel?.numberOfLines = 0
ui.setTitle("Click\nC", for: .normal)
ui.backgroundColor = UIColor.brown
return ui
}()
let labelA: UILabel = { () -> UILabel in
let ui = UILabel()
ui.text = "Test"
return ui
}()
let viewA: UIView = { () -> UIView in
let ui = UIView()
ui.backgroundColor = UIColor.red
return ui
}()
override init(frame: CGRect) {
super.init(frame: frame)
addUI()
addConstrain()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addUI() {
self.addSubview(buttonA)
self.addSubview(buttonB)
self.addSubview(buttonC)
self.addSubview(labelA)
self.addSubview(viewA)
}
func addConstrain() {
buttonA.snp.makeConstraints { (make) in
make.left.top.equalToSuperview()
make.height.equalTo(60)
}
buttonB.snp.makeConstraints { (make) in
make.left.equalTo(buttonA.snp.right)
make.top.right.equalToSuperview()
make.width.equalTo(buttonA.snp.width)
make.height.equalTo(buttonA.snp.height)
}
buttonC.snp.makeConstraints { (make) in
make.left.equalTo(15)
make.right.equalTo(-15)
make.bottom.equalTo(-15)
make.height.equalTo(50)
}
labelA.snp.makeConstraints { (make) in
make.left.equalTo(15)
make.top.equalTo(buttonA.snp.bottom).offset(15)
make.width.equalTo(195)
make.height.equalTo(50)
}
viewA.snp.makeConstraints { (make) in
make.left.equalTo(labelA.snp.right)
make.top.equalTo(buttonA.snp.bottom).offset(15)
make.right.equalTo(-15)
make.height.equalTo(50)
make.bottom.equalTo(buttonC.snp.top).offset(-10)
}
}
func updateConstrain(sender: UIButton) {
switch sender {
case buttonA:
viewA.snp.updateConstraints { (make) in
make.height.equalTo(50)
}
case buttonB:
viewA.snp.updateConstraints { (make) in
make.height.equalTo(150)
}
default:
break
}
}
}
class TestTableViewCell: UITableViewCell {
let cellContainView: CellContainView = { () -> CellContainView in
let ui = CellContainView()
ui.backgroundColor = UIColor.orange
return ui
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.addSubview(cellContainView)
cellContainView.snp.makeConstraints { (make) in
make.left.top.equalTo(15)
make.bottom.right.equalTo(-15)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView: UITableView = { () -> UITableView in
let ui = UITableView()
return ui
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
tableView.register(TestTableViewCell.self, forCellReuseIdentifier: "TestTableViewCell")
tableView.tableFooterView = UIView()
self.view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.top.left.right.bottom.equalToSuperview()
}
}
#objc func buttonAClicked(sender: UIButton) {
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index) as! TestTableViewCell
cell.cellContainView.updateConstrain(sender: sender)
tableview.reloadData()
}
#objc func buttonBClicked(sender: UIButton) {
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index) as! TestTableViewCell
cell.cellContainView.updateConstrain(sender: sender)
tableview.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TestTableViewCell", for: indexPath) as! TestTableViewCell
cell.cellContainView.buttonA.addTarget(self, action: #selector(buttonAClicked), for: .touchUpInside)
cell.cellContainView.buttonB.addTarget(self, action: #selector(buttonBClicked), for: .touchUpInside)
return cell
}
}
Update constrain problem when I click butttonB (Add update viewA heightConstrain & tableview.reloadData()):
TestCellUpdateHeight[29975:5195310] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<SnapKit.LayoutConstraint:0x6040000a4320#CellContainView.swift#75 UIButton:0x7fa1d0d0d680.height == 60.0>",
"<SnapKit.LayoutConstraint:0x6040000a49e0#CellContainView.swift#89 UIButton:0x7fa1d0d10610.height == 50.0>",
"<SnapKit.LayoutConstraint:0x6040000a51c0#CellContainView.swift#104 UIView:0x7fa1d0d11130.height == 150.0>",
"<SnapKit.LayoutConstraint:0x6040000a4800#CellContainView.swift#80 UIButton:0x7fa1d0d0f7b0.top == TestCellUpdateHeight.CellContainView:0x7fa1d0d0d470.top>",
"<SnapKit.LayoutConstraint:0x6040000a4a40#CellContainView.swift#82 UIButton:0x7fa1d0d0f7b0.height == UIButton:0x7fa1d0d0d680.height>",
"<SnapKit.LayoutConstraint:0x6040000a4ec0#CellContainView.swift#88 UIButton:0x7fa1d0d10610.bottom == TestCellUpdateHeight.CellContainView:0x7fa1d0d0d470.bottom - 15.0>",
"<SnapKit.LayoutConstraint:0x6040000a5100#CellContainView.swift#102 UIView:0x7fa1d0d11130.top == UIButton:0x7fa1d0d0f7b0.bottom + 15.0>",
"<SnapKit.LayoutConstraint:0x6040000a5220#CellContainView.swift#105 UIView:0x7fa1d0d11130.bottom == UIButton:0x7fa1d0d10610.top - 10.0>",
"<SnapKit.LayoutConstraint:0x6040000a52e0#TestTableViewCell.swift#26 TestCellUpdateHeight.CellContainView:0x7fa1d0d0d470.top == TestCellUpdateHeight.TestTableViewCell:0x7fa1d208a000.top + 15.0>",
"<SnapKit.LayoutConstraint:0x6040000a53a0#TestTableViewCell.swift#27 TestCellUpdateHeight.CellContainView:0x7fa1d0d0d470.bottom == TestCellUpdateHeight.TestTableViewCell:0x7fa1d208a000.bottom - 15.0>",
"<NSLayoutConstraint:0x6080002819f0 'UIView-Encapsulated-Layout-Height' TestCellUpdateHeight.TestTableViewCell:0x7fa1d208a000'TestTableViewCell'.height == 230 (active)>"
)
Will attempt to recover by breaking constraint

In general, to update the height constraint of a uiview:UIView you should save it first:
var viewHeightConstraint: Constraint!
uiview.snp.makeConstraints { (make) in
viewHeightConstraint = make.height.equalTo(50).constraint
}
If you simply use updateConstraints function, it will add a new height constraint that causes the issue Unable to simultaneously satisfy constraints. So you have to remove the last one:
viewHeightConstraint.deactivate()
Then make constraint again:
uiview.snp.makeConstraints { (make) in
viewHeightConstraint = make.height.equalTo(100).constraint
}

You cannot update cell's height just by changing its constraint.
Table can only update the height of its cells as a result of reloadData() call, after which it will ask its delegate for cell's height (or calculate automatically, if your cells are self-sizing).
So, to get the effect you want you might do something like the following.
In buttonBClicked method remember the state you want to get (using some variable maybe).
In table delegate's method tableView(_:, heightForRowAt:) return the correct height for the cell.
This is not ideal and serves just as a direction for considering your solution. My main point is that you need to reload table view to change cell's height.

What you currently do
labelA.snp.updateConstraints { (make) in
make.bottom.equalTo(buttonC.snp.top).offset(-100)
}
will increase the bottom distance of the labelA , but will leave it's top constant the same you need to either
1- Update the height of the labelA
or
2- Update the height of the redView in front of it
Also don't forget to call
cell.cellContainView.updateConstrain(sender: sender)
cell.layoutIfNeeded()
note also because of cell reusing you may find other cell stretched , so you need to keep track of stretched cell indices and apply that inside cellForRowAt
You can also do this
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat
return expandArr[indexPath.row] ? 300 : 100
}
where
var expandArr = [Bool]()
indicating whether the cell is expanded or not , also if you have a model you may add a bool value to it instead of this separate arr ( it's for illustration )

If you need recalculate height cell (without recreate cells / reload cells) just call:
tableView.beginUpdates()
tableView.endUpdates()

i think you don’t need to change Bottom Constraint.what you required is Apply height Constraint to redView.and change it according to your button Selection.
func updateConstrain(sender: UIButton) {
switch sender {
case buttonA:
redView.snp.updateConstraints { (make) in
make.heigh.equalTo(10)
}
case buttonB:
redView.snp.updateConstraints { (make) in
make.heigh.equalTo(100)
}
default:
break
}
}
}
And don’t forget to reload Specific Row after updating Constraint
#objc func buttonAClicked(sender: UIButton) {
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index) as! TestTableViewCell
cell.cellContainView.updateConstrain(sender: sender)
yourtableview.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.none)
}
#objc func buttonBClicked(sender: UIButton) {
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index) as! TestTableViewCell
cell.cellContainView.updateConstrain(sender: sender)
yourtableview.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.none)
}

Related

SnapKit - UITableView height is not change with dynamical content of cell

I'm a beginner in SnapKit, I want to implement a UITableViewController with SnapKit, each row have two UILabel, one of them is Title and another one is Value.
My issue is the height of the row in UITableView not changed according to the content of each row.
here is my code:
class ViewController:
import UIKit
import SnapKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// MARK: Property List
var list: NSMutableArray?
let myTableView: UITableView = {
let table = UITableView()
return table
}()
//MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.myTableView.rowHeight = UITableView.automaticDimension
self.myTableView.estimatedRowHeight = 100
title = "TableView Page"
setup()
setupViews()
}
// MAKR: Setup View
func setup() {
self.view.backgroundColor = .white
navigationController?.navigationBar.prefersLargeTitles = true
}
func setupViews () {
self.view.addSubview(myTableView)
myTableView.snp.makeConstraints { make in
make.top.left.bottom.right.equalTo(10)
}
myTableView.register(CustomCell.self, forCellReuseIdentifier: CustomCell.customCell)
myTableView.delegate = self
myTableView.dataSource = self
}
// MARK: TableView DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.customCell, for: indexPath) as! CustomCell
cell.title.text = "title is lognest??"
cell.value.text = (indexPath.row % 2 == 0) ?
"longest value longest value longest value longest value longest value longest value longest value for text"
: "short value"
cell.title.snp.makeConstraints { (make) in
make.top.equalTo(cell.value.snp.top)
make.left.equalTo(20)
make.trailing.equalTo(cell.value.snp.leading)
}
cell.value.snp.makeConstraints { (make) in
make.right.equalTo(-20)
make.top.equalTo(cell.title.snp.top)
make.bottom.equalTo(-10)
}
NSLog("value height is: \(cell.value.frame.height)")
NSLog("cell height is: \(cell.frame.height)")
return cell;
}
}
class CustomCell:
// MARK: custom cell
class CustomCell: UITableViewCell {
// MARK: Property
static var customCell = "cell"
public var title:UILabel = {
let tit = UILabel()
tit.setContentHuggingPriority(.defaultLow, for: .horizontal)
tit.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
tit.textColor = .black
tit.alpha = 0.6
return tit
}()
public var value:UILabel = {
let val = UILabel()
val.textColor = .black
val.setContentHuggingPriority(.defaultHigh, for: .horizontal)
val.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
val.lineBreakMode = NSLineBreakMode.byWordWrapping
val.numberOfLines = 0
val.alpha = 0.75
return val
}()
// MARK: initializer
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.addSubview(title)
self.addSubview(value)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init?(coder aDecoder: NSCoder)")
}
}
And this is console logs:
snapkitTest[26345:1387159] cell height is: 44.0
snapkitTest[26345:1387159] value height is: 0.0
1- You have to add the label to
self.contentView.addSubview(title)
2- You need to set bottom constraint to contentView
title.snp.makeConstraints { (make) -> Void in
make.trailing.equalTo(value.snp.leading)
make.top.equalTo(self.contentView.snp.top).inset(10)
make.left.equalTo(20)
}
value.snp.makeConstraints { (make) -> Void in
make.right.equalTo(-40)
make.top.equalTo(self.contentView.snp.top).inset(10)
make.bottom.equalTo(-10)
}
Also transfer this constraints to init of cell custom class , as not to re-add constraints every scroll of tableView

Strange behavior with cell construction with Simple TableView Swift

I have spent an insane time to fix this bug in our application. We are currently working in a chat and we have used a tableview for it.
Our tableview works fine until there is a certain amount of messages, after that, the table starts to flick. I coded without storyboards and for this reason I thought that constraints were the cause of trouble. So I decide to make a really simple tableview with some features of our chat tableview (actually our tableview is coredata linked and with a lot of graphic stuffs).
Because I suspected about constraints, I didn't use it in the code bellow just to see everything works fine, but it wasn't the case. In the gif image we can see two undesirable behaviors, the first one is that sometimes the table is re-generated completely, so cells disappear and appear in a very short time (this cause a very annoying flick). The second is no less annoying: cells are duplicated (I think this is for the cell reusability feature) but after a short period of time they are accommodated and everything goes fine.
https://github.com/hugounavez/pizzaWatch/blob/master/videoBug.gif
I tried adding the prepareForReuse() method and delete the views and creates them in the cell again but no results.
This is the example code, you can copy and run it with no problems in a Playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class Modelito{
// This is the class tableview model
var celda: String
var valor: String
init(celda: String, valor: String){
self.celda = celda
self.valor = valor
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
let tableview: UITableView = {
let table = UITableView()
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Click me to add new cell", for: .normal)
button.setTitle("Click me to add new cell", for: .highlighted)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
var model: [Modelito] = []
let tipoDeCelda = ["MyCustomCell", "MyCustomCell2"]
override func viewDidLoad() {
super.viewDidLoad()
self.setupViews()
self.tableview.register(MyCustomCell.self, forCellReuseIdentifier: "MyCustomCell")
self.tableview.register(MyCustomCell2.self, forCellReuseIdentifier: "MyCustomCell2")
self.tableview.dataSource = self
self.tableview.delegate = self
self.button.addTarget(self, action: #selector(self.addRow), for: .touchUpInside)
// Here I generate semi random info
self.dataGeneration()
}
func setupViews(){
self.view.addSubview(self.tableview)
self.tableview.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.tableview.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50).isActive = true
self.tableview.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1).isActive = true
self.tableview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.tableview.backgroundColor = .gray
self.view.addSubview(self.button)
self.button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.button.topAnchor.constraint(equalTo: self.tableview.bottomAnchor).isActive = true
self.button.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.button.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
self.button.backgroundColor = .orange
}
func dataGeneration(){
let number = 200
// Based in the cell types availables and the senteces, we create random cell info
for _ in 0...number{
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
}
self.tableview.reloadData()
// After we insert elements in the model we scroll table
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
#objc func addRow(){
// This function insert a new random element
self.tableview.beginUpdates()
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
// After inserting the element in the model, we insert it in the tableview
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.insertRows(at: indexPaths, with: .none)
self.tableview.endUpdates()
// Finally we scroll to last row
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
}
extension ViewController{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.model.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 150
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let celldata = self.model[indexPath.row]
switch celldata.celda {
case "MyCustomCell":
let cell = tableview.dequeueReusableCell(withIdentifier: "MyCustomCell", for: indexPath) as! MyCustomCell
cell.myLabel.text = self.model[indexPath.row].valor
return cell
default:
let cell = tableview.dequeueReusableCell(withIdentifier: "MyCustomCell2", for: indexPath) as! MyCustomCell2
cell.myLabel.text = self.model[indexPath.row].valor
return cell
}
}
}
class MyCustomCell: UITableViewCell {
var myLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
myLabel.backgroundColor = UIColor.green
self.contentView.addSubview(myLabel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
myLabel.frame = CGRect(x: 25, y: 0, width: 370, height: 30)
}
}
class MyCustomCell2: UITableViewCell {
var myLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
myLabel.backgroundColor = UIColor.yellow
self.contentView.addSubview(myLabel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
myLabel.frame = CGRect(x: 0, y: 0, width: 370, height: 30)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()
Thanks in advance.
Edit:
I changed the code base in the #Scriptable answer in order to be compatible with playground. At this point, I am starting to think that duplication cell bug is actually normal for tableview. In order to see the problem, the button should be pressed several times fastly.
This code is reloading the tableview but it's removing that "annoying flick" and it's too smooth.
To fix that annoying flick just change addRow() function to
#objc func addRow(){
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
self.tableview.reloadData()
self.scrollToBottom()
}
scrollToBottom() function:
func scrollToBottom(){
DispatchQueue.global(qos: .background).async {
let indexPath = IndexPath(row: self.model.count-1, section: 0)
self.tableview.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
Just try it.
Using your current code in a playground produced a fatal error for me.
2017-11-13 15:10:46.739 MyPlayground[4005:234029] *** Terminating app due to uncaught exception 'NSRangeException', reason: '-[UITableView _contentOffsetForScrollingToRowAtIndexPath:atScrollPosition:]: row (200) beyond bounds (0) for section (0).'
I changed the dataGeneration function to the following code which just calls reloadData once the data has been generated and scrolls to the bottom.
I think the error was that without reloading, there wasn't that many rows to scroll to.
func dataGeneration(){
let number = 200
// Based in the cell types available and the sentences, we create random cell info
for _ in 0...number{
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
}
self.tableview.reloadData()
// After we insert elements in the model we scroll table
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
Once I got around the crash, the tableView worked fine for me. No flickering or duplication.

UITableViewCell doesn't update height after adding a view to UIStackView

I have a UIStackView inside UITableViewCell's contentView. Based on user interaction, I add/remove items in the UIStackView. After modifying the items in UIStackView, I expect the cell to update it's height accordingly. But, it doesn't update it's height unless I call tableView.reloadData(). But, calling reloadData() in cellForRowAtIndexPath / willDisplayCell becomes recursive.
What is the proper way to adjust the cell height at run time based on items in UIStackView?
I use UITableViewAutomaticDimension
Updating the Problem:
Here is a simple prototype of what I am trying to do.
My actual problem is dequeuing the cell.
In the prototype, I have 2 reusable cells and 3 rows. For row 0 and 2, I dequeue cellA and for row 1, I dequeue cellB. Below is the overview on the condition I use.
if indexPath.row == 0 {
// dequeue cellA with 2 items in stackView
}
if indexPath.row == 1 {
// dequeue cellB with 25 items in stackView
}
if indexPath.row == 2 {
// dequeue cellA with 8 items in stackView
}
But the output is,
row 0 contains 2 items in stackView - expected
row 1 contains 25 items in stackView - expected
row 2 contains 2 items in stackView - unexpected, row 0 is dequeued
I also tried removing all arranged subViews of stackView in cellForRowAtIndexPath. But, doing so, flickers the UI when scrolling. How can I manage to get the desired output?
I believe the problem is when you are adding views to the stack view.
In general, adding elements should take place when the cell is initialized.
willDisplay cell: is where one handles modifying attributes of cell contents.
If you move your code from willDisplay cell: to cellForRowAt indexPath: you should see a big difference.
I just made that one change to the code you linked to, and the rows are now auto-sizing based on the stack view contents.
Edit: Looked at your updated code... the issue was still that you are adding your arrangedSubviews in the wrong place. And you compound it by calling reloadData().
Second Edit: Forgot to handle previously added subviews when the cells are reused.
Updated code... replace your ViewController code with:
//
// ViewController.swift
//
import UIKit
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
tableView.estimatedRowHeight = 56
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if indexPath.row == 0 || indexPath.row == 2 {
cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
if let stackView = cell.viewWithTag(999) as? UIStackView {
let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black
// cells are reused, so clear out any previously added subviews...
// but leave the first view that is part of the cell prototype
while stackView.arrangedSubviews.count > 1 {
stackView.arrangedSubviews[1].removeFromSuperview()
}
// use "i" so we can count
for i in 1...numberOfItemsInStackView {
// use label instead of view so we can number them for testing
let newView = UILabel()
newView.text = "\(i)"
newView.textColor = .yellow
// add a border, so we can see the frames
newView.layer.borderWidth = 1.0
newView.layer.borderColor = UIColor.red.cgColor
newView.backgroundColor = color
let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
heightConstraint.priority = 999
heightConstraint.isActive = true
stackView.addArrangedSubview(newView)
}
}
}
if indexPath.row == 1 {
cell = tableView.dequeueReusableCell(withIdentifier: "lastCell")!
if let stackView = cell.viewWithTag(999) as? UIStackView {
let numberOfItemsInStackView = 25
// cells are reused, so clear out any previously added subviews...
// but leave the first view that is part of the cell prototype
while stackView.arrangedSubviews.count > 1 {
stackView.arrangedSubviews[1].removeFromSuperview()
}
// use "i" so we can count
for i in 1...numberOfItemsInStackView {
// use label instead of view so we can number them for testing
let newView = UILabel()
newView.text = "\(i)"
newView.textColor = .yellow
// add a border, so we can see the frames
newView.layer.borderWidth = 1.0
newView.layer.borderColor = UIColor.red.cgColor
newView.backgroundColor = UIColor.darkGray
let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
heightConstraint.priority = 999
heightConstraint.isActive = true
stackView.addArrangedSubview(newView)
}
}
}
return cell
}
// override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// if cell.reuseIdentifier == "cell" {
// if let stackView = cell.viewWithTag(999) as? UIStackView {
// let numberOfItemsInStackView = (indexPath.row == 0) ? 2 : 8
// let color = (indexPath.row == 0) ? UIColor.gray : UIColor.black
// guard stackView.arrangedSubviews.count == 1 else { return }
// for _ in 1...numberOfItemsInStackView {
// let newView = UIView()
// newView.backgroundColor = color
// let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 54)
// heightConstraint.priority = 999
// heightConstraint.isActive = true
// stackView.addArrangedSubview(newView)
// }
// tableView.reloadData()
// }
// }
//
// if cell.reuseIdentifier == "lastCell" {
// if let stackView = cell.viewWithTag(999) as? UIStackView {
// let numberOfItemsInStackView = 25
// guard stackView.arrangedSubviews.count == 1 else { return }
// for _ in 1...numberOfItemsInStackView {
// let newView = UIView()
// newView.backgroundColor = UIColor.darkGray
// let heightConstraint = newView.heightAnchor.constraint(equalToConstant: 32)
// heightConstraint.priority = 999
// heightConstraint.isActive = true
// stackView.addArrangedSubview(newView)
// }
// tableView.reloadData()
// }
// }
// }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
}
Try to reload only the cell using: https://developer.apple.com/documentation/uikit/uitableview/1614935-reloadrows
Example code
Here is an example. We have basic table view cells (TableViewCell) inside a view controller. The cells have 2 labels inside a stack view. We can hide or show the second label using the collapse/reveal methods.
class TableViewCell : UITableViewCell {
#IBOutlet private var stackView: UIStackView!
#IBOutlet private var firstLabel: UILabel!
#IBOutlet private var secondLabel: UILabel!
func collapse() {
secondLabel.isHidden = true
}
func reveal() {
secondLabel.isHidden = false
}
}
class ViewController : UIViewController {
#IBOutlet var tableView: UITableView!
fileprivate var collapsedCells: Set<IndexPath> = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 128
}
#IBAction private func buttonAction(_ sender: Any) {
collapseCell(at: IndexPath(row: 0, section: 0))
}
private func collapseCell(at indexPath: IndexPath) {
if collapsedCells.contains(indexPath) {
collapsedCells.remove(indexPath)
} else {
collapsedCells.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
extension ViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
if collapsedCells.contains(indexPath) {
cell.collapse()
} else {
cell.reveal()
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
}

Custom TableViewCell triggers NSLayoutConstraint error 'UIView-Encapsulated-Layout-Height'

I use Automatic Height for cells.
And each time I want to update my cell with new data (model variable here) I updated my autolayout constraints and I get an error.
Here just to show the issue, I don't even change the constraints.
I simply ask to recalculate the layout.
At the first init of the cell : No warning, no problem.
The error:
<NSLayoutConstraint:0x174285d70 'UIView-Encapsulated-Layout-Height'
UITableViewCellContentView:0x1030f8a50.height == 500 (active)>
The code:
tableView.rowHeight = UITableViewAutomaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 500
}
class TestTableViewCell: UITableViewCell {
var model: X? {
didSet {
setNeedsLayout()
layoutIfNeeded()
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let viewtop = UIView()
let viewbottom = UIView()
viewtop.backgroundColor = UIColor.yellow
viewbottom.backgroundColor = UIColor.red
contentView.backgroundColor = UIColor.blue
contentView.addSubview(viewtop)
contentView.addSubview(viewbottom)
viewtop.snp.makeConstraints { (make) in
make.top.equalTo(contentView)
make.left.right.equalTo(contentView)
make.height.equalTo(50)
}
viewbottom.snp.makeConstraints { (make) in
make.top.equalTo(viewtop.snp.bottom)
make.left.right.equalTo(contentView)
make.bottom.equalTo(contentView)
make.height.equalTo(120)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The question:
Why, after asking a re-layout of the same constraints, do I get an error ?
EDIT: another example for better comprehension here.
var botViewHeightConstraint:Constraint!
class TestTableViewCell: UITableViewCell {
var model: Int? {
didSet {
if model == 1 {
botViewHeightConstraint.update(offset:200)
}else{
botViewHeightConstraint.update(offset:120)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let viewtop = UIView()
let viewbottom = UIView()
viewtop.backgroundColor = UIColor.yellow
viewbottom.backgroundColor = UIColor.red
contentView.backgroundColor = UIColor.blue
contentView.addSubview(viewtop)
contentView.addSubview(viewbottom)
viewtop.snp.makeConstraints { (make) in
make.top.equalTo(contentView)
make.left.right.equalTo(contentView)
make.height.equalTo(50)
}
viewbottom.snp.makeConstraints { (make) in
make.top.equalTo(viewtop.snp.bottom)
make.left.right.equalTo(contentView)
make.bottom.equalTo(contentView)
botViewHeightConstraint = make.height.equalTo(120).constraint
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CellForRow code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = fetchedResultsController?.fetchedObjects?[(indexPath as NSIndexPath).section] {
let cell = tableView.dequeueReusableCell(withIdentifier: imagePostCellId) as! TestTableViewCell!
cell.model = 1
return cell
}
}
First, you keep neglecting to set your subviews' translatesAutoresizingMaskIntoConstraints to false. That's important. (Perhaps snapkit does that for you, however. I don't know.)
Second — and this is the big point — what you're doing is not how you size a variable height cell from the inside out. You do not change an absolute height constraint, as your code is doing. The sizing is based ultimately on the intrinsic content size of the subviews. Some subviews might have absolute heights, certainly, but ultimately there must be at least one with an intrinsic content size. That is the size that you are able to change dynamically in order to determine the height of a cell.
That is why, for example, a cell containing a UILabel is so easy to use with dynamic row heights. It has an intrinsic content size.
The intrinsic content size does not conflict with the built-in height of the cell (the UIView-Encapsulated-Layout-Height in your console dump); it supplements it (when the runtime calls systemLayoutSizeFitting(UILayoutFittingCompressedSize) behind the scenes, which is how automatic variable row heights works).
If you use custom subviews with an implementation of intrinsicContentSize, and if setting your model value in the cell triggers a call to invalidateIntrinsicContentSize, your example will work perfectly with no complaints in the console.
Here is an example of such a custom view:
class MyView : UIView {
var h : CGFloat = 200 {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width:300, height:self.h)
}
}
When this is a subview of your cell's content view, setting this view's h in cellForRow sizes the cell's height correctly.
For example, let's suppose our cell's content view has just one subview, v, which is a MyView. Then:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
let even = indexPath.row % 2 == 0
cell.v.backgroundColor = even ? .red : .green
cell.v.h = even ? 40 : 80 // triggers layout!
return cell
}
Why are you adding "make.height.equalTo(120)" when you've already set top bottom left and right constrain relative to content and top view
As per your code it seems the cell height is always 50+120.
Also see if overriding heightForRow and return UITableViewAutomaticDimension works .

Make UITableViewCell actions overlap content

I was trying to go about making the actions for a UITableViewCell overlap the content of the cell, like in the National Geographic app:
table view
table view when swiped
I tried using a listener on the contentView of the cell to track the frame and keep it constant, but I was unable to get that to work (although it's possible it would, I'm kinda new to iOS).
If anyone has any suggestions for creating a similar effect, they would be much appreciated!
u can make your own custom cell and add a delete button and swipe gestures to make like button overlap the contents of the cell for example, try it out yourself, first create a sample project with single view app, and proceed
subclass the tabview cell with xib option selected and name it something like CustomCell , and in CustomCell.swift class past below code
import UIKit
class CustomCell: UITableViewCell {
var deleteButton:UIButton!
//create a custom cell if not in the reuse pool
class func customCell() -> CustomCell?
{
let aVar:Array = NSBundle.mainBundle().loadNibNamed("CustomCell", owner: nil, options: nil)
if aVar.last!.isKindOfClass(UITableViewCell)
{
return aVar.last as? CustomCell
}
return nil
}
//handle your left swipe
func swipeLeft()
{
print("swipe Left")
self.contentView.bringSubviewToFront(deleteButton!)
var frameRect:CGRect! = deleteButton!.frame
frameRect.origin.x = self.contentView.bounds.size.width - self.deleteButton!.frame.size.width
UIView.animateWithDuration(0.5) { () -> Void in
self.deleteButton!.frame = frameRect
}
}
//aslo the right swipe
func swipeRight()
{
print("swipe right")
var rect:CGRect! = deleteButton?.frame
rect.origin.x = self.contentView.bounds.size.width
UIView.animateWithDuration(0.5) { () -> Void in
self.deleteButton!.frame = rect
}
}
//hear we are adding the delete button in code, if u want add it in xib or (if u are using storyboard )
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
deleteButton = UIButton.init(type: .Custom)
deleteButton.setTitle("Delete", forState: .Normal)
deleteButton.setTitleColor(UIColor.whiteColor(), forState: .Normal)
deleteButton.backgroundColor = UIColor.redColor()
self.contentView.addSubview(deleteButton)
let gestureLeft:UISwipeGestureRecognizer = UISwipeGestureRecognizer.init(target: self, action: "swipeLeft")
gestureLeft.direction = .Left
self .addGestureRecognizer(gestureLeft)
let gestureRight:UISwipeGestureRecognizer = UISwipeGestureRecognizer.init(target: self, action: "swipeRight")
gestureRight.direction = .Right
self .addGestureRecognizer(gestureRight)
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
//set the initial frame of delete button
override func layoutSubviews() {
super.layoutSubviews()
var rect:CGRect! = deleteButton?.frame
rect.size = CGSizeMake(100, 100)
rect.origin.x = self.contentView.bounds.size.width
deleteButton?.frame = rect
}
}
and make sure the tableview cell height to be of 100pt and in view controller set up a tableview in storyboard with datasource and delegate and implement the required delegate and datasource methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:CustomCell? = tableView.dequeueReusableCellWithIdentifier("CELL") as? CustomCell
if cell == nil
{
cell = CustomCell .customCell()
}
return cell as CustomCell!
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 100
}

Resources