Detect when UITableViewCell goes off the screen - ios

I'm implementing a rich UITableView with customly created UITableViewCell, I show these on the screen in one fashion, but once they go off the screen I want to take a note of that, since the second time they come on I would like them to get displayed in a different manner. Think auto "mark as read" when going off the screen.
I've been looking for some way to detect when a cell goes off the screen (get's deallocated or dequeued or equivalent), preferably in the UITableViewController class to make a quick note of the indexPath.row value, but in the UITableViewCell is equally as good.
I haven't been able to do this in any standard way. Counting the times it appeared seems out of the question as I do multiple reloadData calls on the table.
Anyone any ideas? This seems a bit tricky :)

This is an old question, but in case anyone is looking, in iOS6, a new UITableViewDelegate function was introduced that does just this:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
It does a great job at telling you whenever a cell is removed, however, it is very thorough and thus if you did a reload cell, even the old cell that's being replaced will trigger this delegate function. In my implementation I simply check to see if the indexPath passed is still within the array tableView.indexPathsForVisibleRows. Something like:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([tableView.indexPathsForVisibleRows indexOfObject:indexPath] == NSNotFound)
{
// This indeed is an indexPath no longer visible
// Do something to this non-visible cell...
}
}

I think you could use the
- (NSArray *)visibleCells
method for your UITableView. This returns an array of all cells that are visible. You can then mark any data that is "not visible" (i.e. not in this array) in the way you want, such that when you scroll back to it, it has been updated.
Hope that helps

Once UITableViewCell is invisible, it will be removed from UITableView. You may override the method -(void)removeFromSuperView, and do something within the method. At last, do not forget to call [super removeFromSuperView].

The prepareForReuse method on UITableViewCell that Andrey Tarantsov mentions looks good. Putting a couple of NSLogs in there allows you to print out the values of any variables of the cell. Any thoughts as to how this could be set back to the table view controller?

Are you sure a cell going offscreen is exactly what you want to catch? If you want to mark items as read, this does not seem like a proper way to do it. For example, I might scroll though the table really fast, and I would be very surprised if you marked all of the stuff as read.
As for the technical part, simply keep a list of cells that are on screen (cellForRowAtIndexPath should add cells to that list), and in scrollViewDidScroll delegate method check if any of them are no longer visible.
Another possible idea: I remember there is prepareForReuse method on the cell. Not sure when it is called, though.

I know this is a REALLY old question, but in case anyone is looking for an answer for Swift 5:
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
<#code#>
}

I think I would try periodically checking the indexPathsForVisibleRows property of the UITableView. From the largest index path, you can deduce that all previous rows have been scrolled past.

I needed to get some data from the cell as it was scrolled off of the screen. I used #Mr.T's answer however it doesn't state how to get the data.
Say for example the name of the cell class that I'm using is MyCell and it has a data model in it named MyModel with a property of postId. I initially set that info in cellForItem:
var datasource = [MyModel]()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCell
cell.myModel = datasource[indexPath.item] // an individual instance of MyModel from the array
print("cellForItem - indexPath.item: ", indexPath.item) // if the was the very first cell coming on it would print 0
print("postId: ", cell.myModel.postId) // maybe the postId is qwerty
return
}
To get some data from the cell as it is scrolled off of the screen:
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let myCell = cell as? MyCell else { return } // You must cast the cell from the method param to your cell type which for me is MyCell
print("didEndDisplayingCell - indexPath.item: ", indexPath.item) // if this was the very first cell scrolling off it should print 0
print("postId: ", myCell.myModel.postId) // the postId should be qwerty
}
The best way to test this is to add a small amount of cells to your collectionView, like first 2 cells, then later on 3 cells, then later on 4 cells. Then just scroll off the very first cell and see what is printed out. Do it for each cell. The indexPath.item and postId should both match for cellForItem and didEndDisplaying.

Related

Swift tableview changes the value of a random cell when one cell is edited

I have a long list of textfields so I am using a tableView.
this is how the screen looks like
When I insert some text in a textfield in one of the cells and scroll down some other cells get the same textfield value. I have no idea why this would happen.
This is my code now:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! RelatieToevoegenCell
cell.label.text = items[indexPath.row]
cell.textfield.placeholder = items[indexPath.row]
return cell
}
Your main problem is that you are keeping your data inside Views (UITableVieCell).
Cells are reused by UITableView to optimize performance - so even if you have 1milion rows in your table, only few UITableViewCells are in memory (as many as are visible on the screen usually).
When you are calling dequeueReusableCell it takes one of already existing cells and reuse it.
To solve this problem you need to keep your data separately in an Array and keep modified texts there.
Then you need to modify code you posted, to take data every single time you configure UITableView from your "dataSource".
Also a good pratcice is to reset UITableViewCell when it's reused, you can do this by adding coding inside prepareForReuse - but that's optional, as text will be set from your data source anyways.
I would also recommend to read this before you start coding further - it will allow to understand how UITableView works on iOS:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/TableView_iPhone/TableViewCells/TableViewCells.html#//apple_ref/doc/uid/TP40007451-CH7-SW1
Basically, you have to get and store the values in view-controller because due to the reusable behavior of UITableViewCell you lost the reference of the invisible cell with all the child reference.
So you can store the text-field value by the textViewDidChange action in RelatieToevoegenCell class.

Disabling all UITableViewCells user interaction

Hey I have a UITableView. There are 4 large cells (so they are not all displayed on the screen). At various points through the app I want to disable user interaction for all of them however I am getting a nil unwrap error when I run this code:
for row in 0...3 {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: 2)) as! AnswerTextChoiceCell
cell.userInteractionEnabled = reEnable ? true : false
}
I'm guessing it's because it can't fetch the cell because it's not displayed on the screen. How Would I go about disabling all user interaction for all the cells?
I don't want to disable the user interaction on the tableView as it will prevent the user from scrolling.
Any pointers on this would be really appreciated!
Try once.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Configure the cell
if indexPath.row == yourcell {
cell.userInteractionEnabled = false
}
return cell!
}
Hope this helps.
Without seeing the rest of your code and your TableView the only two things I can see that would cause that crash would be:
When you are casting the cell from 'cellForRowAtIndexPath' to your 'AnswerTextChoiceCell' type
You have the wrong section passed in to your 'cellForRowAtIndexPath' function
To try and see which of the two it is I would first try omitting the 'AnswerTextChoiceCell' cast completely and see if the problem goes away, or at least that it stops crashing. The function will return a UITableViewCell anyway and the userInteractionEnabled property is available on that object anyway so this will still allow you to test out what you're after.
If that doesn't work then we'll need a bit more information about your table view such as, how many sections do you have, is it storyboard based, etc.

How to prevent cell reusing for the text label?

I have a table view and in the cell I put text label that indicates likes count. When I load the app, for example, likes count shows 3. But when I scroll down and then return to the top, those 3 becomes a different number.
I know that it's because of cell reusing. How can I save those numbers for each cell and do not update them on scroll?
Images I\ve cached with AlamofireImage, but I do not know what to do with these text labels
In the cellForRowAtIndexPath, you can use the indexPath to get the correct value back.
I cannot see your code, so if you are just using a number like 1, 2, 3, 4, etc, you can do
Example:
cell.label.text = indexPath.row + 1
or if you're using an array, you could do the following
cell.label.text = array[indexPath.row]
Hope this helps!
The part you are missing is that you need to use the row index to index into your data. Obviously we can't see your code, but it sounds like you are not using this. Generally, you have an array of data, and in cellForRowAtIndexPath, you use the index to lookup the correct data element.
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath) as! MyCell
cell.textLabel.text = "\(likesCount)"
return cell
}
Is your cell a subclass of UITableViewCell?
Give your cell an Identifier (reuse identifier) via IB and then
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCellSubclass *cell = [tableView dequeueReusableCellWithIdentifier:#"your_identifier"];
// .. your code
}
And redefine the method in your subclass
- (void)prepareForReuse
{
self.yourCountLabel.text = #"0"; // "reset" value
}
You don't prevent it (unless you want to absolutely kill the performance of your application). You embrace it.
In cellForRowAtIndexPath, you get a cell that has been used before for a different. So you change everything that you don't want to be the same as for an unknown other row.

Configuring UICollectionView to keep the same cell view

I have a collection view delegate
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath)
-> UICollectionViewCell {
}
I wanted the property of the cell to persist between each call to this method.
let cell = levelView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier,
forIndexPath: indexPath)
But retrieving cell using dequeueReusableCellWithReuseIdentifier seems to create a new cell view element that is different from the initial one.
cell.accessibilityIncrement()
So the cell accessibility value cannot persist after tapping on the cell.
I need to do this because I wanted to do UI testing by tapping on the cell.
Any help is appreciated!
Reusable cells in iOS are handled with a queue. This helps the tableViews and collectionViews move smoothly and remain efficient with a large amount of data. I don't think you would want to change the way that is designed to work in your application. This way, the device only needs to load as many cells into memory as can be displayed on a single screen. As cells move off the screen, they can be reused as another cell as cellForItemAtIndexPath will be called to load the required data into the reused cell.
If the goal is to have a value persisted between cell reloads, consider using a map or some other similar variable that could be managed by the collectionView's dataSource delegate.
For example, in your viewController that contains your dataSource delegate, you could have a Dictionary defined like so:
let numberOfTapsByIndexPath = [NSIndexPath: Int]()
Then every time a cell is tapped, you would increment the value in the map for the cell tapped. That code might look something like this:
let previousTaps = numberOfTapsByIndexPath[indexPath]
numberOfTapsByIndexPath[indexPath] = previousTaps + 1
I'm not 100% sure that was the goal you explained above, but regardless, you'll want to move any persistent information out of the cell and into a variable that the dataSource delegate or some other singleton can manage.
Hope this helps!

UICollectionViewCells deque returns wrong cell

I have a CollectionView, I use the cells to display videos and I also store some other data on variables. And whenever I scroll I reuse previous cells using the following code:
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("profileCell", forIndexPath: indexPath) as! ProfileCollectionViewCell
But I noticed that when I am using an indexPath I have previously used, I do not get back the same cell as previously but another cell apparently at a random order. I would like to avoid reloading the videos when it is not necessary.
Is there a way for me to get the same cell used with the same indexPath?
Thanks,
Carlos
That's how collectionView.dequeueReusableCellWithReuseIdentifier works.
If you want to go around this functionality you could save all cells that are created in an array and then instead of dequeueing you could fetch the one with the video already loaded. However, if you use some framework that caches the videos after loading them you shouldn't need to deal with this at all.
var cells: [CustomCollectionViewCell] = []
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell:CustomCollectionViewCell?
if cells.count >= indexPath.row {
cell = cells[indexPath.row]
} else {
cell = CustomCollectionViewCell()
//setup cell
cells.append(cell)
}
return cell!
}
Disclaimer:
This solution is a hack. As mentioned by #Async in the comment below this solution will use a lot of memory if you create a lot of cells.
You are using reusable cells. With that you can't be sure you got always the same cell, it's almost always the other way around. You could try with storing the cells when first loaded, then checking it in cellForRow.. but in my opinion better way would be to store videos to file (of course with a limit), because storing cells isn't the best option.
You cannot influence which cell instance you get back from dequeueReusableCellWithReuseIdentifier. So you cannot keep the video in the cell when the cell is scrolled offscreen.
What you can do is add a NSMutableDictionary property to your view controller and store references to your videos in it (use the cell's indexPath as key). You can store the references in
tableView(_:didEndDisplayingCell:forRowAtIndexPath:).
Then when you get a cell from dequeueReusableCellWithReuseIdentifier you retrieve to video from the dictionary and add it to the cell. That way it does not matter which cell you get back from the queue.

Resources