weird tableview behavior when cell is selected - ios

My tableview has a weird behavior when a cell is selected but this behavior is not seen always. When a cell has been selected some cells which are below the selected cell moves. These two gifs will show you the behavior in the two cases when it is shown and when it doesn't appear.
this is the
tableview without weird behavior and this is the tableview with the weird behavior
and this is my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : UserTableViewCell = (tableView.dequeueReusableCell(withIdentifier: "userCell") as? UserTableViewCell)!
if(self.firebaseUsers.count != 0){
cell.influentBackgroudView.layer.cornerRadius=10
let url = URL.init(string: String().loadCorrectUrl(url:firebaseUsers[indexPath.row].image))
cell.influentImageView.contentMode = .scaleAspectFit
cell.influentImageView!.sd_setImage(with: url!, placeholderImage: UIImage.init(named: "placeholder"),options: [.continueInBackground,.progressiveDownload], completed: { (image, error, cacheType, imageURL) in
if (error != nil) {
cell.influentImageView!.image = UIImage(named: "placeholder")
} else {
cell.influentImageView.contentMode = .scaleAspectFill
cell.influentImageView.image = image
}
})
cell.influentImageView.layer.cornerRadius=10
cell.influentImageView.layer.masksToBounds = true
cell.influentNameLabel.text=" " + firebaseUsers[indexPath.row].name + " "
cell.influentNameLabel.adjustsFontSizeToFitWidth = true
cell.influentNameLabel.textAlignment = .center
if(selectedCellIndex==indexPath ){
cell.influentBackgroudView.isHidden=false
}
else{
cell.influentBackgroudView.isHidden=true
}
cell.selectionStyle = .none
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let previousIndex=selectedCellIndex
selectedCellIndex=indexPath
if(previousIndex==selectedCellIndex){
let nextVC = storyboard?.instantiateViewController(withIdentifier: "VisitedProfileViewController") as! VisitedProfileViewController
nextVC.passedUser = firebaseUsers[selectedCellIndex!.row]
navigationController?.pushViewController(nextVC, animated: true)
}
if(previousIndex==nil){
tableView.reloadRows(at: [indexPath], with:.none)
}
else{
if(previousIndex != indexPath){
tableView.reloadRows(at: [indexPath,previousIndex!], with: .none)
}
else {
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
thank you guys for your help!

As identified from the comments the issue you are facing is produced by calling reloadRows when you press a cell with conjunction of incorrect estimated row heights. So either reloading needs to be removed or estimated height corrected. The first solution is already covered in an answer provided by A. Amini.
Since many of such anomalies are related to estimated row height it still makes sense to improve it.
For simple static row heights you can either implement a delegate method for instance
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return indexPath.row == 0 ? 44.0 : 200.0
}
where the values are exactly the same as in your heightForRowAt method. Or if all rows have same hight you can remove this delegate method but set the heights directly in some initialization method like:
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 200
tableView.estimatedRowHeight = tableView.rowHeight
}
When more complicated cells are introduced and automatic cell height is used we usually use a height cache of cells. It means we need to save height of a cell when it disappears so we may use it later. All heights are saved in a dictionary of type [IndexPath: CGFloat]. A very simple implementation should look like this:
private var cellHeightCache: [IndexPath: CGFloat] = [IndexPath: CGFloat]()
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? UITableView.automaticDimension
}
In some cases extra work is needed like clearing the cache when table view is reloaded.
The reason why this is happening is because table view will not reload cells around the cells you currently see but rather check the content offset and compute which cells you were supposed to be seeing. So any call to reload may make the table view jump or animate cells due to wrong estimated row height.

The main problem was you're reloading tableView after "each" selection.
Try this code out and let me know if you need more help.
class MyTableViewClass: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "userCell" as! UserTableViewCell
if !self.firebaseUsers.isEmpty {
cell.influentBackgroudView.layer.cornerRadius = 10
cell.influentImageView.contentMode = .scaleAspectFit
let url = URL(string: firebaseUsers[indexPath.row].image)
cell.influentImageView!.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"),options: [.continueInBackground,.progressiveDownload], completed: { (image, error, cacheType, imageURL) in
if error != nil {
cell.influentImageView.contentMode = .scaleAspectFill
}
})
cell.influentImageView.layer.cornerRadius=10
cell.influentImageView.layer.masksToBounds = true
cell.influentNameLabel.text = " \(firebaseUsers[indexPath.row].name) "
cell.influentNameLabel.adjustsFontSizeToFitWidth = true
cell.influentNameLabel.textAlignment = .center
cell.selectionStyle = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let nextVC = storyboard?.instantiateViewController(withIdentifier: "VisitedProfileViewController") as! VisitedProfileViewController
nextVC.passedUser = firebaseUsers[indexPath.row]
navigationController?.pushViewController(nextVC, animated: true)
}
}
class UserTableViewCell: UITableViewCell {
override var isSelected: Bool {
didSet {
if isSelected {
// Selected behavior, like change Background to blue
} else {
// Deselected behavior, change Background to clear
}
}
}
}

Related

TableView Cell doesn't scale equally along y-axis

I have tableView cells that are populated with a color in each cell. What I want is when the user taps on the cell, it "opens"/expands so that that color fills the entire screen. Currently, it only scales downwards from the cell that I click on. I also need it to scale upwards along the y-axis, each cell expanding to the top of the screen, but I'm not sure what's prohibiting it to.
let expandedColorView: UIView = {
let view = UIView()
return view
}()
#objc func userTap(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizer.State.ended {
let tapLocation = sender.location(in: self.paletteTableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
if let tappedCell = self.tableView.cellForRow(at: tapIndexPath) {
UIView.animate(withDuration: 1.0, animations: {
tappedCell.transform = CGAffineTransform(scaleX: 1, y: 50)
} )
}
}
}
}
UITapGestureRecognizer is declared in tableView(cellForRowAt:) with cell.isUserInteractionEnabled = true enabled.
I've tried changing the expandedColorView bounds self.expandedColorView.bounds.size.height = UIScreen.main.bounds.height in UIView.animate but that doesn't change anything. I was thinking the cell's frame would need to change so that it matches the parent view frame (which I think would be tableView) but I couldn't figure out how to do that.
Any help would be appreciated!
I've attached a gif of the issue:
If that's what you want
This is what I have done in data source extension
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = self.tableView.dequeueReusableCell(withIdentifier: "colorCell") as? ColorFulTableViewCell {
let color = colors[Int(indexPath.row % 7)]
cell.backgroundColor = color
return cell
}
return UITableViewCell()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let row = expandCell?.row, row == indexPath.row {
return self.tableView.bounds.size.height
}
else {
return 100
}
}
}
And tableView delegate extension looks like
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Whats happening her?
In heightForRowAt of TableView data source method, I check if cell need to cover the whole tableView size by using if let row = expandCell?.row, row == indexPath.row { and set its height to match the tableView height by returning self.tableView.bounds.size.height else I return 100
In didSelectRowAt I update the indexPath of cell to expand by saving it in expandCell and I reload the row (so that this time when height for row is called it can return self.tableView.bounds.size.height and I also call scrollToRow(at with position as .top to ensure my cell scrolls to top and makes itself visible completely
Because you are reloading only a specific cell, though from cost perspective its efficient, but animation might look rusty as other cells in visible indexPath array are adjusting them selves abruptly, you can always call reload Data to get much better smoother experience.
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.expandCell == indexPath { return }
else {
self.expandCell = indexPath
}
self.tableView.reloadData()
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Hope this helps

Jerk while scrolling when tableView inside tableview Swift

I am using UITableview inside tableView for one of my screen. Here I have one, InstalmentMainTableViewCell and InstalmentInnerTableViewCell.
I used below code to scroll inner tableView with full height:
class InnerTableView: UITableView {
override var intrinsicContentSize: CGSize {
//This is for extra space after inner tableview size. can be required
self.layoutIfNeeded()
return self.contentSize
}
}
Now, the problem is when I am scrolling from to second cell of MainTableViewCell from first it is getting stuck for a second and never happens again. It is only happening for first time whenever the view-controller appears.
Here is the full code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch (tableView.tag) {
case 100:
return instalmentModel.count == 0 ? 0 : instalmentModel.count
default:
return instalmentModel[currentInstalmentIndex].EMIDetailModel.count == 0 ? 0 : instalmentModel[currentInstalmentIndex].EMIDetailModel.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch (tableView.tag) {
case 100:
currentInstalmentIndex = indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: InstalmentsMainTableViewCell.className) as! InstalmentsMainTableViewCell
if let model = self.instalmentModel[indexPath.row] as InstalmentModel? {
if tableView.visibleCells.contains(cell) {
self.putValue(self.yearLabel, "\(String(describing: model.year!))")
}
cell.emiTotal,text = "\(model.year!)"
}
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: InstalmentsInnerTableViewCell.className) as! InstalmentsInnerTableViewCell
if let model = self.instalmentModel[self.currentInstalmentIndex].EMIDetailModel[indexPath.row] as EMIDetailModel? {
cell.indicatorView.backgroundColor = UIColor.red
}
return cell
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let indexPath = self.instalmentsTableView.indexPathsForVisibleRows?[0]
if let model = instalmentModel[(indexPath?.row)!] as InstalmentModel? {
putValue(yearLabel, "\(model.year!)")
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.view.layoutIfNeeded()
self.view.setNeedsLayout()
}
The requirement is:
Requirement for tableView inside tableview

UITableViewCell unintentionally placing checkmark on unwanted rows

I am making a music genre picking application and when I go to my table to select genres, I select a row and it selects a random row about 10 or so down from my selection.
My code for the selection is:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let genresFromLibrary = genrequery.collections
let rowitem = genresFromLibrary![indexPath.row].representativeItem
print(rowitem?.value(forProperty: MPMediaItemPropertyGenre) as! String
)
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .checkmark
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath)
{
cell.accessoryType = .none
}
}
Cells are reused by default when cellForRowAtIndexPath is called. This causes the cells to have the wrong data when you don't keep track of the indexPaths that have been selected. You need to keep track of the index paths that are currently selected so you can show the appropriate accessory type in your table view.
One way of doing it is to have a property in your UITableViewController that just stores the index paths of the selected cells. It can be an array or a set.
var selectedIndexPaths = Set<IndexPath>()
When you select a row on didSelectRowAt, add or remove the cell from selectedIndexPaths, depending on whether the index path is already in the array or not:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) {
// The index path is already in the array, so remove it.
selectedIndexPaths.remove(indexPathIndex)
} else {
// The index path is not part of the array
selectedIndexPaths.append(indexPath)
}
// Show the changes in the selected cell (otherwise you wouldn't see the checkmark or lack thereof until cellForRowAt got called again for this cell).
tableView.reloadRows(at: [indexPath], with: .none)
}
Once you have this, on your cellForRowAtIndexPath, check if the indexPath is in the selectedIndexPaths array to choose the accessoryType.
if selectedIndexPaths.contains(indexPath) {
// Cell is selected
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
This should solve the problem of the seemingly random cells that are checked every 10 cells down or so (which, is not random, it's just that the cell with the checkmark is being reused).
Because cellForRow returns a cached cell you generated. When scrolling out of the screen the order of cells are changed and cells are reused. So it seems "randomly selected".
Don use cellForRow, instead record selection data.
Here's code works in a single view playground.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController, UITableViewDataSource, UITableViewDelegate {
let tableView = UITableView()
var selection: [IndexPath: Bool] = [:]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
view.addSubview(tableView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = self.view.bounds
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "c")
if let sc = cell {
sc.accessoryType = .none
let isSelected = selection[indexPath] ?? false
sc.accessoryType = isSelected ? .checkmark : .none
return sc
}
return UITableViewCell(style: .default, reuseIdentifier: "c")
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.textLabel?.text = NSNumber(value: indexPath.row).stringValue
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selection[indexPath] = true
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Image not getting Proper height inside UITableViewCell ios Swift 4

I am trying to download image from server and want to load images inside my cell but as i am downloading inside cellForRowAt method it wont get height for the first time. If i scroll up and scroll down again the image will have proper height.
Using Kingfisher to download images from server
var homeList = [NSDictionary]()
var rowHeights : [Int:CGFloat] = [:]
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return homeList.count
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.rowHeights[indexPath.row]{
print(" Height \(height)")
return height
}
else{
return 160
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let homeObject = homeList[safe: indexPath.row] {
if let dynamicURL = homeObject["dynamic_card_url"] as? String, dynamicURL != "" {
tableView.register(UINib(nibName: "DynamicCell", bundle: nil), forCellReuseIdentifier: "\(indexPath.row)")
let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.row)", for: indexPath) as! DynamicCell
KingfisherManager.shared.downloader.downloadImage(with: URL(string: dynamicURL)!, options: .none, progressBlock: nil, completionHandler: { (image, error, url, data) in
DispatchQueue.main.async {
if (image != nil || url != nil){
let aspectRatio = (image! as UIImage).size.height/(image! as UIImage).size.width
cell.dynamicImageView.image = image
let imageHeight = self.view.frame.width*aspectRatio
self.rowHeights[indexPath.row] = imageHeight
}else{
print("Image or URL is nil")
}
}
})
cell.selectionStyle = .none
cell.backgroundColor = UIColor.clear
return cell
}
}
}
When you downloaded your image you should reload your cell to change it size to appropriate one. You get right sizes as you scrolling because tableView calls heightForRowAt when it needs to display new cell. So inside in DispatchQueue.main.async { reload the cell after setting all necessary properties UITableView().reloadRows(at: [IndexPath], with: UITableViewRowAnimation.automatic)
I used this guys suggestion: https://stackoverflow.com/a/33499766/8903213
code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.imageView?.kf.setImage(with: URL(string: urlOfPhoto)!, placeholder: PlaceholderImages.user, completionHandler: {
(image, error, cacheType, imageUrl) in
cell.layoutSubviews()
})
...
and this seems to be working for me.

Dynamic Dimension of UITableViewCell ok, but how hide them if empty?

I have a UITableView with 3 prototyped cells (ex. 1st cell: image, 2nd cell: Description, 3. Links,...).
I would like to hide them if for a cell the data from the backend is empty (Ex. if there is no image, hide the first cell). In order to do that, I have override the heightForRowAtIndexPath function in this way:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch (indexPath.row) {
case 0:
if event?.photo_urls.count == 0{
return 0
}
else{
return 80.0
}
case 1:
if event?.description == ""{
return 0
}
else{
return 90.0
}
default:
return 100.0
}
}
and hidden the cell by doing
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch (indexPath.row) {
case 0:
let cell = tableView.dequeueReusableCellWithIdentifier("PhotoCell", forIndexPath: indexPath) as! UITableViewCell
if event?.photo_urls.count != 0 {
// ...
}
else{
cell.hidden = true
}
return cell
case 1:
let cell = tableView.dequeueReusableCellWithIdentifier("DesCell", forIndexPath: indexPath) as! UITableViewCell
if event?.description != "" {
// ...
}
else{
cell.hidden = true
}
return cell
default:
let cell = tableView.dequeueReusableCellWithIdentifier("PhotoCell", forIndexPath: indexPath) as! UITableViewCell
return cell
}
}
Until here no problem, it works properly!
Now, THE PROBLEM is that I would like to make the cells dynamics according to the cell contents (ex. description height). In order to do that, I have used
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 80.0
}
and if I comment the heightForRowAtIndexPath the cells are actually dynamics but I can't hide them anymore.
Do you have any suggestion on how to be able to hide the cells if they are empty and apply the automatic dimension according to their content?
lets say you have dynamic data and you want to show it in tableview so you need to create an array of your data to display.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet
var tableView: UITableView
var items: [String] = ["We", "Heart", "nothing" ,"" ,"imageurl", "", "xyz"]
override func viewDidLoad() {
super.viewDidLoad()
reloadTableAfterSorting()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func reloadTableAfterSorting(){
for var i = 0; i < self.items.count; i++
{
if self.items[i] == ""{
self.items.removeAtIndex(2)
}
}
self.tableView.reloadData()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println("You selected cell #\(indexPath.row)!")
}
}
For that i recommend you to sort the array before displaying it in the table view. Hiding the cell is not a good idea and its not good according to Apple recommendations. So you can do one thing except hiding the cell: remove the index from the array. In this way you can always have data to show in table and it will behave properly. So don’t try to hide the cell just pop the index from array.

Resources