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.
Related
I'm pretty new to iOS dev and I have an issue with UITableViewCell.
I guess it is related to dequeuing reusable cell.
I added an UIImageView to my custom table view cell and also added a tap gesture to make like/unlike function (image changes from an empty heart(unlike) to a filled heart(like) as tapped and reverse). The problem is when I scroll down, some of the cells are automatically tapped. I found out why this is happening, but still don't know how to fix it appropriately.
Below are my codes,
ViewController
import UIKit
struct CellData {
var title: String
var done: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.textLabel?.text = model.title
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadData()
}
}
TableViewCell
import UIKit
class TableViewCell: UITableViewCell {
let mainVC = ViewController()
static let identifier = "TableViewCell"
let likeImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(likeImage)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImage.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
likeImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
likeImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
if likeImage.image == UIImage(systemName: "heart.fill"){
likeImage.image = UIImage(systemName: "heart")
likeImage.tintColor = .darkGray
} else {
likeImage.image = UIImage(systemName: "heart.fill")
likeImage.tintColor = .systemRed
}
}
}
This gif shows how it works now.
enter image description here
I've tried to use "done" property in CellData structure to capture the status of the uiimageview but failed (didn't know how to use that in the correct way).
I would be so happy if anyone can help this!
You've already figured out that the problem is cell reuse.
When you dequeue a cell to be shown, you are already setting the cell label's text based on your data:
cell.textLabel?.text = model.title
you also need to tell the cell whether to show the empty or filled heart image.
And, when the user taps that image, your cell needs to tell the controller to update the .done property of your data model.
That can be done with either a protocol/delegate pattern or, more commonly (particularly with Swift), using a closure.
Here's a quick modification of the code you posted... the comments should give you a good idea of what's going on:
struct CellData {
var title: String
var done: Bool
}
class ShinViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(ShinTableViewCell.self, forCellReuseIdentifier: ShinTableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ShinTableViewCell.identifier, for: indexPath) as! ShinTableViewCell
let model = models[indexPath.row]
cell.myLabel.text = model.title
// set the "heart" to true/false
cell.isLiked = model.done
// closure
cell.callback = { [weak self] theCell, isLiked in
guard let self = self,
let pth = self.tableView.indexPath(for: theCell)
else { return }
// update our data
self.models[pth.row].done = isLiked
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
class ShinTableViewCell: UITableViewCell {
// we'll use this closure to communicate with the controller
var callback: ((UITableViewCell, Bool) -> ())?
static let identifier = "TableViewCell"
let likeImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// we'll load the heart images once in init
// instead of loading them every time they change
var likeIMG: UIImage!
var unlikeIMG: UIImage!
var isLiked: Bool = false {
didSet {
// update the image in the image view
likeImageView.image = isLiked ? likeIMG : unlikeIMG
// update the tint
likeImageView.tintColor = isLiked ? .systemRed : .darkGray
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// make sure we load the heart images
guard let img1 = UIImage(systemName: "heart"),
let img2 = UIImage(systemName: "heart.fill")
else {
fatalError("Could not load the heart images!!!")
}
unlikeIMG = img1
likeIMG = img2
// add label and image view
contentView.addSubview(myLabel)
contentView.addSubview(likeImageView)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImageView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
// let's use the "built-in" margins guide
let g = contentView.layoutMarginsGuide
// image view bottom constraint
let bottomConstraint = likeImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
// this will avoid auto-layout complaints
bottomConstraint.priority = .required - 1
NSLayoutConstraint.activate([
// constrain label leading
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
// center the label vertically
myLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view trailing
likeImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain image view to 30 x 30
likeImageView.widthAnchor.constraint(equalToConstant: 30),
likeImageView.heightAnchor.constraint(equalTo: likeImageView.widthAnchor),
// constrain image view top
likeImageView.topAnchor.constraint(equalTo: g.topAnchor),
// activate image view bottom constraint
bottomConstraint,
])
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
// toggle isLiked (true/false)
isLiked.toggle()
// inform the controller, so it can update the data
callback?(self, isLiked)
}
}
I'm attempting to use a custom UITableViewCell that I created programmatically in a UITableView.
The UITableViewCell class:
class PopulatedCellStyleTableViewCell: UITableViewCell {
private let userDisplayName: UILabel = {
let lbl = UILabel()
lbl.font = UIFont(name: "Baskerville-Bold", size: 16)
lbl.textColor = .purple
lbl.textAlignment = .left
lbl.backgroundColor = UIColor(displayP3Red: 1, green: 1, blue: 1, alpha: 0.5)
return lbl
}()
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: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(userDisplayName)
//from a UIKit extension that allows me to easily add anchors
userDisplayName.anchor(top: self.topAnchor, left: self.leftAnchor, bottom: self.bottomAnchor, right: self.rightAnchor, paddingTop: 5, paddingLeft: 0, paddingBottom: 5, paddingRight: 0, width: self.frame.width, height: self.frame.height, enableInsets: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And the UITableViewController class:
class LoveListController: UITableViewController {
var loveList: [String]!
let populatedCell = "populatedCell"
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(PopulatedCellStyleTableViewCell.self, forCellReuseIdentifier: populatedCell)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return loveList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: populatedCell, for: indexPath)
// Configure the cell...
cell.textLabel?.text = loveList[indexPath.row]
return cell
}
}
Upon inspecting the debug view hierarchy, the labels load in, but the actual text is rendered in a UITableViewCellContentView instead of in my UILabel, and I have no idea why.
If this is easily solvable or has previously been discussed, please point me in the right direction and/or teach me how to discover the solution myself!
You need to cast the cell and use your custom userDisplayName not textLabel
let cell = tableView.dequeueReusableCell(withIdentifier: populatedCell, for: indexPath) as! PopulatedCellStyleTableViewCell
cell.userDisplayName.text = loveList[indexPath.row]
plus it's better to add the label to contentView and create the constraints with it
self.contentView.addSubview(userDisplayName)
self.userDisplayName.anchor(top: self.contentView.topAnchor .....
I have a UIViewController with a navigation bar and a tab bar. Other than that, the whole screen is made up of a UITableView.
I have a large tableHeaderView that has the same background color as the navbar.
When I drag the content up (scrolling down) everything looks fine.
But if I drag it up, there is an ugly disconnection between the navigation bar and the header view.
Is there any way I could anchor it to the top when dragging down, while allowing it to scroll when dragging up?
You can try creating a view and placing it behind the tableView, as the table view scrolls, the height of the view is updated.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
lazy var tableView : UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
let backView : UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
var backViewHeight : NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ViewController"
self.view.addSubview(backView)
backView.translatesAutoresizingMaskIntoConstraints = false
backView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
backView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
backView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
backViewHeight = backView.heightAnchor.constraint(equalToConstant: 0)
backViewHeight?.isActive = true
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
tableView.register(Cell.self, forCellReuseIdentifier: "cell")
tableView.register(Header.self, forHeaderFooterViewReuseIdentifier: "header")
tableView.backgroundColor = .clear
self.navigationController?.navigationBar.barTintColor = .red
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.setValue(true, forKey: "hidesShadow")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
backViewHeight?.constant = -scrollView.contentOffset.y
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header")
header?.contentView.backgroundColor = .red
let headerLabel = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: 100))
headerLabel.textAlignment = .center
headerLabel.text = "Header"
header?.addSubview(headerLabel)
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let view = UIView()
view.backgroundColor = .white
return view
}
}
class Cell: UITableViewCell {
let label : UILabel = {
let label = UILabel()
label.text = "One Label"
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
setupViews()
}
func setupViews() {
self.backgroundColor = .white
self.addSubview(label)
label.frame = self.frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class Header : UITableViewHeaderFooterView {
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If you copy paste this code in an empty project you can have a look at the behavior. Don't forget to embed the ViewController in a NavigationController. Hope it helps
1) If an unwanted white space on the top of tableview is permanent and the constraints are correct this is the solution.
the scroll view insets adjusted automatically if you disable it should remove it
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
2) if u just have it when u pull down and it goes back to its normal state. It means is that the tableview bouncing is enabled and that is normal behaviour according to iOS documentation:
If the value of this property is true, the scroll view bounces when it encounters a boundary of the content. Bouncing visually indicates that scrolling has reached an edge of the content. If the value is false, scrolling stops immediately at the content boundary without bouncing. The default value is true.
you can uncheck the bouncing from the tableview in your storyboard or xib file. Or u can use this snippet:
tableView.bounces = false
tableView.alwaysBounceVertical = false
Note: that is not recommended to disable the scroll bouncing since it would make things feel very unnatural for iOS.
and also if you want to use pull to refresh it will not work.
So finally if u choose to not disable it you will have to change the background color of the parent of your tableview and it will solve it.
I hope that makes sense!
Gif depicting that :
When user selecting table view cell for first time(checkbox ticked first time) , cell getting selected but after that it is deselected automatically and nothing happens when i am tapping second time.
But when i am tapping third time cell getting selected properly and on 4th tap it is deselecting properly and so on for 5th , 6th time onwards.
My didSelectRowAt() method looks like this :
func expandableTableView(_ expandableTableView: LUExpandableTableView, didSelectRowAt indexPath: IndexPath) {
let cell = expandableTableView.cellForRow(at: indexPath) as! FilterTableCell
let dictKey : String = FilterKeysMapping[FilterKeysFront.object(at: indexPath.section) as! String]!
if(self.FilterDictAPI[dictKey] == nil){
self.FilterDictAPI[dictKey] = [indexPath.row: self.FilterValueArray.object(at: indexPath.row)]
}
else{
self.FilterDictAPI[dictKey]![indexPath.row] = self.FilterValueArray.object(at: indexPath.row)
}
self.expandableTableView.beginUpdates()
cell.button.isSelected = true
self.expandableTableView.reloadRows(at: [indexPath], with: .automatic)
self.expandableTableView.endUpdates()
expandableTableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
}
didDeselectRowAt() method is like this :
func expandableTableView(_ expandableTableView: LUExpandableTableView, didDeselectRowAt indexPath: IndexPath) {
print("Did Deselect Cell at section \(indexPath.section) row \(indexPath.row)")
let cell = expandableTableView.cellForRow(at: indexPath) as! FilterTableCell
cell.button.isSelected = false
let dictKey : String = FilterKeysMapping[FilterKeysFront.object(at: indexPath.section) as! String]!
if(self.FilterDictAPI[dictKey] != nil){
self.FilterDictAPI[dictKey]?.removeValue(forKey: indexPath.row)
}
print("dict after removing values : \(self.FilterDictAPI)")
}
cellForRowAt() method is :
func expandableTableView(_ expandableTableView: LUExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = expandableTableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as? FilterTableCell else {
assertionFailure("Cell shouldn't be nil")
return UITableViewCell()
}
cell.selectionStyle = UITableViewCellSelectionStyle.none
cell.label.text = "\(self.FilterValueArray.object(at: indexPath.row))" + " (" + "\(self.FilterCountArray.object(at: indexPath.row))" + ")"
return cell
}
Table View Cell is :
class FilterTableCell: UITableViewCell {
let label = UILabel()
let button = UIButton()
var check = Bool()
// MARK: - Init
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Base Class Overrides
override func layoutSubviews() {
super.layoutSubviews()
label.frame = CGRect(x: 42, y: 0, width: contentView.frame.width-42, height: contentView.frame.height)
self.label.font = UIFont(name: "PlayfairDisplay-Regular", size: 18)
button.frame = CGRect(x:10, y: contentView.frame.height/2-8, width: 16, height: 16)
button.setImage(UIImage(named: "CheckboxUnchecked"), for: .normal)
button.setImage(UIImage(named: "CheckboxChecked"), for: .selected)
button.setImage(UIImage(named: "CheckboxUnchecked"), for: .highlighted)
}
}
Issue as mentioned is only this : After first tap it is deselecting automatically.
What's exactly happening is in didSelectRowAt when you reload that indexPath, that cell is deselected automatically and didDeselectRowAt method is called where cell.button.isSelected = false removes the checkmark.
So, to fix this comment out the below lines in didSelectRowAt method.
self.expandableTableView.beginUpdates()
self.expandableTableView.reloadRows(at: [indexPath], with: .automatic)
self.expandableTableView.endUpdates()
Also, reset button's selected state in cell's prepareForReuse() method. This will fix the undefined behaviour where checkbox is selected randomly or after first or second taps.
override func prepareForReuse() {
super.prepareForReuse()
button.isSelected = false
}
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)
}