Why is my custom collection view cell unexpectedly nil after selecting cell in code? - ios

So I have a colectionView of images, and when something happens in the background, I might try to select a specific custom collectionViewCell using the method:
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically), which works fine, the collectionView scrolls to the desired location.
However, if I then try to actually update the appearance of the cell as it's been updated by calling self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload) I get an unexpectedly nil cell when I then try to create the cell in didSelectItemAtIndexPath.
I partially understand why this method of updating cells is unsafe (as I've read elsewhere in researching this question like here in one of the answers.)
Thus, the crash makes me assume that cells are not part of the visible cells on the screen, which is why the cell is nil when I try to create it. But this doesn't make sense as I also assume that the cells have to be created in order to be scrolled to, which as I said works fines because they are created as expected and can be interacted with without issue.
So why is my cell nil? Or why is my collection view not thinking that the cell that was scrolled to not part of the visible cells? And if the reason is obvious, then how can I make a cell update it's appearance when I select it in code?
EDIT: Code in context
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically)
self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload)
return
}
As I've said, this pretty much is the context. In the first line I may scroll to an index that is not visible on the screen. If I do this, and then the second line of code executes, the cell that is created in the delegate method that is called is unexpectedly nil.

In order to fix this issue, I had to use kind of a hacky workaround, which although works seems almost too dirty, and I don't know why this issue hasn't been addressed by Apple (i.e why selectItemAtIndexPath doesn't call the delegate method didSelectItemAtIndexPath). Anyways, what I ended up doing was when I needed to update my selected cell in background, I first got the index and set a bool to show a cell was selected in code:
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.selectItemAtIndexPath(indexPathToReload, animated: true, scrollPosition: UICollectionViewScrollPosition.CenteredVertically)
let cell = self.collectionView.cellForItemAtIndexPath(indexPathToReload) as? ListingCollectionViewCell
if cell != nil {
self.collectionView(self.collectionView, didSelectItemAtIndexPath: indexPathToReload)
return
} else {
self.buttonSelectedInCode = true
self.indexPathSelectedInCode = indexPathToReload
return
}
}
Above, I had to try to create the cell for the specified index path. If the cell isn't nil then I know that the cell is visible, and it's safe to call on the delegate didSelectItemAtIndexPath. However, if the cell is nil, then I have to set up my bool and index, and wait for the scroll view to call the delegate method, as shown below.
Then, I further implemented scrollViewDidEndScrollAnimation and used the call of this delegate method to then select my cell in code, as follows:
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
if buttonSelectedInCode {
self.collectionView(self.collectionView, didSelectItemAtIndexPath: self.indexPathSelectedInCode)
}
}

Related

Why does referencing an item that exists in a collection view return a nil value?

let x = X(name: "x")
blocks.append(newBlock)
let indexPath = IndexPath(row: blocks.count - 1, section: 0)
collectionView.insertItems(at: [indexPath])
let aCell = collectionView.cellForItem(at: indexPath) as! CollectionViewCell
The above code is in a function that runs when a button is pressed in the view controller to present a new item in the collection view. As collectionView.insertItems(at: [indexPath])adds a new item at the specific index, I don't understand why let aCell = collectionView.cellForItem(at: indexPath) as! CollectionViewCellwould return a nil value. The specific error is "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value". I only get this error when the cell is outside of the screen. Meaning that if I add an item when there are few cells on the screen or if I scroll to the bottom if there are many cells and add a new item, it'll work. However, If there are many items and I do not scroll down, it'll have that error
The documentation for cellForItem(at:) says this:
this method returns nil if the cell isn't visible or if indexPath is out of range
You're getting a nil value because your cell is outside of the screen, and the force-cast is causing a crash. You should avoid force-casting in general.
Update
I think you might be unfamiliar with the way tables and collection views work. They don't store a cell for every row/item that you have, instead they create a pool of cells and reuse them.
When the cell goes out of bounds, the table/collection view stops associating it with the IndexPath and, as the user scrolls, this old cell is used again for the new IndexPath that needs to be displayed.
In general, you should configure your cell completely in collectionView(_:cellForItemAt:), and only use cellForItem(at:) to update a visible cell when the content that should be displayed on it changes.
There are 2 similar methods. UICollectionView implements the method cellForItem(at:) and the UICollectionViewDataSource protocol defines the method collectionView(_:cellForItemAt:) They do different things.
The UICollectionView method cellForItem(at:) will return a cell at the specified IndexPath if it is on screen and not out of range. It's meant for fetching cells that are currently visible.
If you call that method when the first 5 cells of your single-section collection view are visible, and ask for the indexPath of the 6th cell, you will get a nil back. That is what's happening in your code. You're trying for force cast nil to CollectionViewCell, so your code crashes. Don't do that. Check to see if you get back a nil, and handle nil gracefully.
If you scroll down so that cell 6 is visible and then ask for cell 6, it will be returned. If it's currently off-screen, it will return nil. As #EmilioPelaez says in his answer (voted), that's how the function works.
The other method collectionView(_:cellForItemAt:) is the method the collection view calls to ask its data source to create and configure cells. That method must always return a cell for any valid IndexPath in your model. You should't call that method directly though.

fatal error: unexpectedly found nil while unwrapping an Optional value in Tableview

I had implemented UITableView with custom cell.It works fine.
Tableviewcell have 3 textfields in every cell.
I try to access all cell on submit button.
But app is crash due to access non-visible cell.
How can I got all cell's all 3 textfield values even they are not visible.
Thank you,
UITableView's cellForRowAtIndexPath method will return nil if cell not visible. You should check for this, and perform only data source changes if so:
if let cell_note = self.tbl.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 4)) as? NotesCell
{
// do something with your data source
// do something with cell_note that already loaded on screen
}
else
{
// do something with your data source
// your cell didn't loaded yet
// you should only prepare fresh information in your data source for delegate method tableView(_:cellForRowAtIndexPath:)
}
Note that if you have same data source changes for both cases (as usual in fact), you need move them out of this check

iOS tableview cell is empty at random

Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.

Custom swipeable Table View Cell, Close other once new one open

I'm following tutorial from raywenderlich (How To Make A Swipeable Table View Cell With Actions) site, that shows you how to create custom cell with layers and delegate.
Now I got everything working correctly, buy I would like one of my cells to close if other cell open, how can I achieve this? or Like Messenger app, don't allow users to open another cell option unless they close the current one.
I can't wrap my head around this, I see few other people also ask the same question in comments, but no one reply.
Anyone with Objective-C knowledge, it's okay, I can translate it to swift myself.
The reason I'm using this tutorial, is because the Apple API doesn't allow custom button (using PaintCode) to be used as Action button.
I find a very simple solution for anyone else who trying to achieve the same method.
Create a Method - closeOtherCells:
Since we store all cells in NSMutableSet we can see which ones are available for closing.
func closeOtherCells(close close: Bool){
if close{
//Check For Available Cell
for cells in cellsCurrentEditing {
//Get Table Cells at indexPath
var cellToClose: CustomTableViewCell = self.tableView.cellForRowAtIndexPath(cells as! NSIndexPath) as! CustomTableViewCell
//Call Reset Method in our Custom Cell.
cellToClose.resetConstraintContstantsToZero(true, notifyDelegateDidClose: true)
}
}
}
Now Simply in your cellDidOpen: delegate method call closeOtherCells: with true parameter.
func cellDidOpen(cell: UITableViewCell) {
//Close Other Cells
closeOtherCells(close: true)
//Store Open Cell in NSMutableSet
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
self.cellsCurrentEditing.addObject(indexPath)
}
I hope this help others. :)

Call UITableViewCell Method from Method in main VC?

I'm trying to do the opposite of what most people on this topic are asking to do. Most people want a button within a table view cell to call a method in their VC / VC table. I already have a protocol doing that.
Problem / Question
What I am trying to add now is the opposite: I need a button press on my main ViewController (which houses my table) to call a method within my CusomTableViewCell class (note: the button pressed on the main VC is not in the table). I have the protocol class created and the function written, but I don't know how to set the CustomCellViewClass as the delegate. When I did the opposite, I inserted "cell.delegate = self" into the cellForRowAtIndexPath method. I've also used prepareForSegue to assign a delegate. But with no segue and now cell-creation-method, I'm lost!
Example of Desired Function
My end goal is that pressing a button that is in the main VC will change the title of a button within the cells. A simple example would be that I have one view with a single table, on button press the table contents switch between two arrays, cars and motorcycles. When the table is showing cars, the cell button titles should all read "Look inside" but when showing the motorcycle button it should read "Look closer".
Code
I've already written the function that I want the cell to execute:
func cellButton_Title_Switch (currentList: String) {
if vcState == "cars" {
cellButton.setTitle("Look inside", forState: UIControlState.Normal)
}
else {
cellButton.setTitle("Look closer", forState: UIControlState.Normal)
}
}
I created the protocol:
protocol delegateToChangeCellBut {
func cellButton_Title_Switch (currentList: String)
}
I have the self.delegate.cellButton_Title_Switch(currentList) within my VC button and the protocol added to my custom cell class declaration. But how do I do that last missing piece in the custom cell class, where I assign the class to the delegate?
My original problem was that my UITableView's cell has buttons and labels, some of which change to match the state of things outside the table, things handled by the mainViewController.
The custom cell is defined by a customCellviewController. All the custom cell buttons and labels have their IBOutlets connected to the customCellviewController. I couldn't figure out how to make an action/change outside the table (in the mainViewController) immediately cause the cell labels and buttons to change.
Note: Protocols tend to work they other way around (a cell action triggers a function in the mainVC). I couldn't figure out how to use a protocol to solve this. Luckily, the solution was much simpler than a protocol.
The Solution!
I wrote the "updateCell" method that would change the labels and buttons and that code now sits in the customCellviewController. Then I called/triggered the "updateCell" function from the mainViewController simply by adding the call into my cellForRowAtIndexPath function. So it looks something like this:
var stateOfPage = "Green"
//Creates the individual cells. If the above function returns 3, this runs 3 times
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//Setup variables
let cellIdentifier = "BasicCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! customCell
cell.updateCell(stateOfPage)
return cell
}
Now the above code/method runs when the table gets built. So to update the cells, have some button tap or other action reload the table data:
tableView.reloadData()
Good luck.

Resources