I'm trying to clear a specific cell for my tableview. I know to achieve that by using this code right here:
let index = IndexPath(row: 1, section: 0)
if let cell = self.tableView.cellForRow(at: index) as? SomeTableViewCell {
// do sth
}else {
print("Row 1 not found")
}
But the problem is when I use this method with a bigger screen phone like Iphone7, 8, X... It'll be working fine but when I tested on iphone5, 6 or the smaller screen that Row 1 is not visible, It'll print that "Row 1 not found"
Seems like you've almost found the problem yourself!
As with tableviews, the iOS reuses cells them to preserve memory and to offer a seamless smooth scrolling experience to the user.
The iOS reuses the cells when they disappear from the screen. ie: When you scroll down, the initial cells start to be reused. In your larger screens, this takes a while to occur as it may be displaying 20 cells at once vs 10 screens (at once) on your smaller screens.
If you want to permanently remove this cell, then you should remove the corresponding data object from your model which backs up the table view (datasource) and reload the tableview.
If you don't want to permanently remove this cell, then you can reset it only when the table actually needs it to be drawn. This can be done in the datasource method
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell
//Do your Resetting code here
}
Related
I have a problem with reusing a cell.
In my cell there is a view that changes the constraints depending on its state. By default, cell height = 0, when the user clicks on the button, it changes its constraints top, leading, trailing and bottom. The problem is that if I expand, press expand view in one cell, scroll down (or up), then another cell will also be expanded.
I change the variable responsible for uncovering the cell in the prepareForReuse method, and it doesn't help :(
My solution used to be like this: I redid the constraints in the configure method (it is called in the cellForRow method in VC), and it worked, BUT there were lags when scrolling, apparently, because I change the constraints every time a cell is configured and reused
Are there ways to avoid lag when scrolling? Is there another way to do this?
I use SnapKit for make layout.
I would try avoiding making the cell have a height of zero. This may mess with table view and create an overhead in multiple situations. Since cells are dequeued to reduce resource consumptions you should not produce zero-sized cells as theoretically you can see infinite number of such cells in screen. So basically you can easily have a situation where hundreds of cells are being processed by your table view which user is not even seeing.
An alternative to zero-height cell is to insert/delete cells from table view. There are even animations for that already in place. To achieve this you need to correct your numberOfRows method and your cellForRowAt where both of them need to ignore "hidden" cells. Beside that you basically just need something like
func hideCell(indexPath: IndexPath) {
tableView.beginUpdates()
self.items[indexPath].isHidden = true
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
func showCell(indexPath: IndexPath) {
tableView.beginUpdates()
self.items[indexPath].isHidden = false
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
This should fix most (all) of your issues. But if you still want to stay with having small/zero sized cells there are other ways to fix your issue.
When you change your constraints you should also call (maybe you already do that but did not post it)
cell.remakeParentCommentConstraints()
tableView.beginUpdates()
tableView.endUpdates()
and make sure that when cellForRow is being called you also call cell.remakeParentCommentConstraints() because as cell is dequeued you can not know what state the previous cell was in.
Even when doing all of this correctly you may experience jumping. This is now because estimated height logic is not very smart by default and you need to improve it. A quick fix is to cache your cell heights:
private var cellHeightCache: [IndexPath: CGFloat] = [:]
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? 44.0
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
The part with 44.0 should be whatever you have currently set as estimated row height.
This trick may get a bit more complicated if you insert/delete rows (as in first solution). As you also need to insert/remove heights from cache. It is doable, just not as easy.
I solved the problem by dividing the states into different cells
In my tableview, every cell will be different and determined by a JSON response from server. And there will be infinite possibilities. So defining a prototype for each type of cell is not possible.
For example, one cell will have labels and buttons, another cell have images and buttons in different orders.
How to achieve this dynamic structure in tableview cells?
Currently what I am doing is: adding views as subview in cellForRowAtIndexPath but scrolling is very laggy this way.
How to achieve this without affecting performance this much
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! MyCell
for myview in data[indexPath.row].myviews{
cell.addSubview(myview)
}
return cell
}
If you're using a table view then your content is going to scroll vertically, right?
There is a physical limit to the amount of UI that you can put horizontally. Limited by the screen size.
So I'm guessing your UI parts are being laid out vertically in the cell?
So instead of laying out a button, label, image, another button, and a text field vertically in a cell...
Create a cell type called ButtonCell, LabelCell, ImageCell, MultiLineLabelCell, TextFieldCell, etc...
So now, instead of creating one cell with all these elements added. You instead create multiple cells each containing one type of UI. Now you can dequeue your cells in any particular order (driven by your JSON) and won't lose the performance.
The only solution I see is to have empty cell and add/remove subviews as needed. But you should add new subviews to a cell only if you did not add them before.
For example:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! MyCell
if cell.contentView.viewWithTag(1) == nil {
let label = UILabel()
label.tag = 1
cell.contentView.addSubview(label)
}
let label = cell.contentView.viewWithTag(1)
// label config there
return cell
}
Also don't forget to add subviews to cell's contentView not to cell itself.
I am working on a ViewController with a TableView populated with dynamic cells from a prototype nib. I have run into a dead-end trying to recreate certain a cell-expanding animation.
My goal:
When a cell is selected, the selected cell should "expand" (grow in height to twice it's starting size) while at the same time creating space between the cell directly above and below the selected cell.
I've found an example of EXACTLY the asethetic I am looking for in the app "Things". Below are two screenshots showing the table before and and after a cell is selected:
Screenshot of table BEFORE expansion
Screenshot of table AFTER expansion
The best way I can describe the desired animation is the UITableView version of "the parting of the Red Sea". When a cell is selected, the surrounding cells give way lending the selected cell more room.. and the user's focus.
What I've tried:
I found Simon Lee's method answering a similar question and implemented it into my project. And although it animates the row-height change perfectly, it only pushes the adjacent cells on one side of the selected cell. (ie: if the cell at index 4 is selected, all the cells from index 5+ move down but those from index 0-3 stay static. Thereby not achieving the look I'm seeking.)
Using that method, the relevant sections of my code looked something like:
var cellHeight: CGFloat = 72
var selectedCell: IndexPath?
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath == selectedCell {
return cellHeight * 2
} else {
return cellHeight
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedCell = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
selectedCell = nil
tableView.beginUpdates()
tableView.endUpdates()
}
Because that didn't result in my desired animation, I tried insertRows:at:with: to insert 2 empty cells (1 above and 1 below the selected row) -- then deleteRows:at:with: to remove them upon deselecting the cell. This ultimately made for a better looking animation, and looked closer to the "Things" example I'm shooting for. However this made the table overly complicated because by adding and removing cells each time a row is selected, it would change the index of the other cells making it frustrating to predict which cells would have what index at any given time.
A possible solution idea?
After working on this for a couple of days the only other way I could think to accomplish what I want is to somehow scroll the table slightly at the same time that the selected cell's height it changed.. so that as the cell expands (moving the following cells downward) it would make the previous cells appear to move upward. I'm hesitant to try this because it feels like a hack, there should be a better way to accomplish this.
ANY help would be thoroughly appreciated! I've been pulling my hair out at an alarming rate. Thank you to anyone who can share their knowledge.
I have dynamic cell height in my tableView
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
I want to do same as whatsapp app (pagination), i want to load 25 by 25 row so when user scroll at top, i want to load new data but i want to keep same position of scroll, here is what i did
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isMore && scrollView.contentOffset.y == 0 {
loadMore() // function that add new data in my model
self.tableView.reloadData()
}
}
The problem that i have is after i reach top of my tableview it call reloadData but the tableView scroll to UP
I tester some other solution like this:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let height = cellHeights[indexPath] else { return 70.0 }
return height
}
But still not working
If you are reloading the table to add more data , then after reload call below function to retain the scroll position.
tableview.scrollToRow(at: indexPath, at: .middle, animated: true)
here indexPath is the indexPath of the last cell before reload.
In tableView:cellForRowAt when you detect indexPath is getting close to the last element (when indexPath.row is getting close to yourArray.count), query for more items. When you have them, add them to your data source (the array, or whatever it is, where you have all the objects represented in the table) and reload the table view. The table view should display the new items and the user can continue scrolling without doing anything special.
You shouldn't wait until tableView:cellForRowAt is called for the last cell, as the user will have to wait if you need to call a server to fetch more items. Whatsapp's scroll is fast because they download the items well before you scroll to the last elements (some time ago it did stop at the end, but now they are probably fetching new items way earlier).
Keep in mind that 25 items might be too few, it the network is slow the user might scroll through the items you have before more items to display and will have to wait, which is not a good user experience. Simulate a slow network connection and see if 25 items are enough to scroll through while your app contacts the server (depends on network speed, request and response sizes, how fast the server answers your request, etc.).
I faced the same problem in a messaging App. Here's how I solved it:
Assigned an unique messageId to every message in datasource array.
In cellForRow:, I save that messageId of very first row in a variable called messageIdFirst.
if(indexPath.row == 0){
messageIdFirst = message.id!
}
In loadMore() method, I check which current indexpath row have the same messageId in now updated datasource array.
if(message.id == messageIdFirst){
previousTopIndexPath = i
}
,where i being that index in datasource array which have the same messageId.
Now, I scroll to that previousTopIndexPath like this:
tableview.scrollToRow(at: previousTopIndexPath, at: .middle, animated: true)
Note 1: As this was not working efficiently in UITableView, I end up using UICollectionView with the same solution.
Note 2: While this does work smoothly, I know there must exist a cleaner way to do this.
I am building a card type layout with a UITableView, the "cards" need to be able to resize and be swiped to remove them. I implemented the delegate calls for both actions and the resizing is done as follows:
The "preferred height" is set right before the call to actually resize.
//supply card heights to cell
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//if no cards are in memory put them in memory.
if (cards == nil){
populateCardData()
}
let card = cards![indexPath.row]
return card.preferedHeight
}
And the call to resize:
func resizeCardWithIndexPath(inIndexPath: NSIndexPath) {
//simply reloads the data at the given index path
shouldAnimate = [inIndexPath:false]//DONT PAY ATTENTION TO THIS(for something else)
tableView.beginUpdates()//THIS IS THE UPDATE PART
tableView.endUpdates()
shouldAnimate = [inIndexPath:true]
}
And The removing of cards:
func removeCardWithIndexPath(inIndexPath:NSIndexPath){
//if no cards are in memory put them in memory.
if (cards == nil){
populateCardData()
}
//remove card from the array
cards!.removeAtIndex(inIndexPath.row)
//delete row from tableview
tableView.deleteRowsAtIndexPaths([inIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
Now...
All of this works. The cell gets resized appropriately and cells are removed correctly. But when resizing or deleting is done, sometimes irritatingly enough, some of the other cells flash while moving in the Table View.
Am I just doing something stupid or is there a better way?
I did a lot of research and tried a few solutions to no avail.
I did find one way to fix the problem is to make the cell background a solid color and not transparent, this fixed the flashing, but is not a solution because the cards need a transparent border so the background can be seen.
If anyone has any insight help will be a life saver!