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.
Related
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.
I am trying to figure out how to detect which UITableViewCell in a UITableView are deployed in my phone screen.
For example: My numberOfRowsInSection are 20 but i see just 3 of them on the phone screen. I want to know how do determine which three are shown?
In my case, I need to send only the publicity that the users viewed in your phone screen and not the 20 rows loaded in the UITableView.
Thanks!
let visibleIndexPaths = tableview.indexPathsForVisibleRows
var visibleCellsArray: [UITableViewCell] = []
for currentIndextPath in visibleIndexPaths! {
// You now have visible cells in visibleCellsArray
visibleCellsArray.append(tableview.cellForRowAtIndexPath(currentIndextPath)!)
}
You now have visible cells in visibleCellsArray.
Your cellForRowAtIndexPath method calls for the number of visible cells only.IF YOU DEQUEUE THEM.
If you want to do something based on your visible cell, either put some logic in cellForRowAtIndexPath or use indexPathsForVisibleRows which will give you back an array of visible cells and you can go on with your logic.
let visibleIndexPaths = self.tableView.indexPathsForVisibleRows() as? [NSIndexPath]
This gives the array of indexPath that the cell are in visible..
i hope this will help you...
To determine the NSIndexPath values for the rows displayed on the screen, consider the answer to this related SO question.
Specifically, to get an array of the visible table cells, use:
self.tableView.indexPathsForVisibleRows
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.
I let user to reorder rows in tableView. Because this event affects content - some numeric values in cells should be updated - in all the other rows, I call a reloadData in moveRowAtIndexPath. And then strange effects occur.
I.e. cells seems overlapping when touching dragger, and some cell starts to move up and down. Its important to know, that cells height are varying.
The strange, that if I remove reloadData from moveRowAtIndexPath, then all these phenomenons disappear. Only the content is invalid.
So how should I reload data after reordering?
UPDATE: What I have done meantime reconfiguring cells in viewDidLayoutSubviews instead of call reloadData end of the moveRowAtIndexPath. And it works 90% like I expect, but still rows are sometimes somewhat higher they should.
override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
//..
reorderOccured = true
}
override func viewDidLayoutSubviews() {
if reorderOccured {
for cell in tableView.visibleCells() as! [UITableViewCell] {
let ip = tableView.indexPathForCell(cell)
if ip != nil {
self.configureCell(cell, indexPath: ip!)
}
}
reorderOccured = false
}
}
You shouldn't call reloadData after reorder.
You have to make the same changes to your data, as the changes that has been made on the screen.
For example: if you moved cell nr 2 to position 6, you have to remove your object that populate cell nr.2 and insert it again at position 6.
You didn't provide enough details, but usually you would keep your data in an array of objects. This array you have to make the changes to, so your backing datasource is valid.
Here's a link to details from Apple.
I just read the updated after I posted my answer. It seems like you really need to reloadData. In this case I advise to reload after a small delay with a dispatch_async block on the main thread. Say after 0.1.
I found here the answer: https://stackoverflow.com/a/4692563/239219
[tableView beginUpdates];
[tableView endUpdates];
This code forces UITableView to reload cell sizes only, but not cell contents.
I can't yet comment on pteofil's answer, but he is correct: if you have a numbered set of rows in a table, and you move one calling moveRow(...), your animation for the move will be cancelled by a tableView.reloadData().
As such I delayed the data reload (which renumbers all the visible cells based on the updated data source (don't forget to do that when you move stuff around!)) a couple hundred milliseconds and it works perfectly now and looks great too.
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.