indexPath Value of UICollectionView - objective-c

When using a UICollectionView, I am perplexed with getting the indexPath value of the didSelectItemAtIndexPath method.
I'm using the following line of code in the didSelectItemAtIndexPath method:
//FileList is an NSArray of files from the Documents Folder within my app.
//This line gets the indexPath of the selected item and finds the same index in the Array
NSString *selectedItem = [NSString stringWithFormat:#"%#",[FileList objectAtIndex:indexPath.item]];
//Now the selected file name is displayed using NSLog
NSLog(#"Selected File: %#", selectedItem);
The problem is that the indexPath always returns 0 (see below code) and as a result only the first item in the FileList NSArray is ever selected.
I've tried different parameters such as
indexPath.row
indexPath.item
and just plain indexPath
ALL of these return a value of 0 in the following NSLog statement:
NSLog(#"index path: %d", indexPath.item); //I also tried indexPath.row here
Maybe I'm just formatting the NSString improperly, however I don't think this is the case as there are no warnings and I've tried formatting it differently in other places.
Why does the indexPath always return 0?
Any help would be appreciated! Thanks!

At the risk of stating the obvious, make sure your code is in the didSelectItemAtIndexPath method rather than didDeselectItemAtIndexPath. You will get very different indexPath values for a given touch event in the latter method and it's quite easy to insert the wrong one with Xcode's code completion.

The delegate method you are using always provides the index path of the cell that was selected.
Try to debug with a breakpoint on your NSLog call. When debugger stops on it look at the Debug Area at the bottom and use the console (View => Debug Area => Activate Console if you need it).
Type in the console: po indexPath
You should see something like this if selected item is the 5th of the list in section:0 (first section and probably the only one)
<NSIndexPath 0xa8a6050> 2 indexes [0, 5]
Hope this helps to figure out what's going on.

Related

collectionView:cellForItemAtIndexPath: doesn't get called

I want to add new cells in my collection view, but nothing shows up when I add data.
I have a custom UICollectionViewLayout class, which has been working just fine, and I've been keeping dummy data in my datasource to adjust the layout. Now that I got rid of the dummy data, nothing's showing up.
Since the app didn't break and there weren't any warnings, it was difficult to track down where the problem was, and here's where I found a clue:
(UICollectionViewLayout class)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSLog(#"ElementsInRect: – Visible cells info: %#", [self.collectionView.visibleCells description]);
...
}
Here, -visibleCells returns an empty array, even when I add data, call -reloadData and invalidate the layout. So I placed a breakpoint in -collectionView:cellForItemAtIndexPath:, and it turns out this method is not called at all. How did the cells show up before?
Any help would be appreciated.
The data source method, collectionView:numberOfItemsInSection:, has to return a non-zero number for collectionView:cellForItemAtIndexPath: to be called. When you had dummy data in your data source, it was. Now that you removed that dummy data, that method is probably returning 0. When you add data, it should put items into your data source, and then a call to reloadData should work. You should put a log in collectionView:numberOfItemsInSection:, and see what it's returning.
Okay, it turns out the issue was in UICollectionViewLayout. I doubt anyone else will be having this problem, but I'll write my answer for the sake of completeness:
I'd been tweaking my custom UICollectionViewLayout class, and after I'd thought that it was working well, I made the code look neat by deleting old code that was commented out, move methods, etc.
While doing that, I recalled having read somewhere that it's good practice to create attributes in -prepareLayout method, and return those attributes when -layoutAttributesForItemAtIndexPath: or -layoutAttributesForElementsInRect: is called. For me, it was a matter of moving a block of code, so I thought no biggie. And during this "cleaning process" I must have made a mistake.
What's really frustrating is that the code itself actually works regardless of where the attributes are created, and I can't tell what went wrong for the last few days.
The following is a snippet of code that I used to create the attributes objects. My initial question was asking why -collectionView:cellForItemAtIndexPath: was not called while executing the 3rd line. I did not change this part of the code, other than moving it around.
for (int i = 0; i < 3; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:self.topLayer];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
if (cell) {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.array addObject:attributes];
} else {
NSLog(#"prepLayout: the cell doesn't exist for the index path {%d – %d}", indexPath.section, indexPath.item);
}
}
Number of Rows in Section - the count that can be used will determine if the cellForItemAtIndexPath gets called.
Initially when the view loads this will be called. Within the numberOfItemsInSection, if you have an array, the [array count] might return a nil value.
Complete the procedure where the array is populated, then reload the data in the collection view which will re-assess the numberOfItemsInSection. This can be done with the following code:
[self.myCollectionView reloadData];
"myCollectionView is the name given to the collection view item in your view"

UICollectionViewFlowLayout's layoutAttributesForItemAtIndexPath returns nil when called "too early"

My setup:
A standard UICollectionView with a flow layout; datasource is set to my controller.
The layout is configured as usual in the xib file.
When I try to access a cell's layout attributes via layoutAttributesForItemAtIndexPath: in the Controller's viewDidLoad method like so:
NSLog(#"LayoutAttribute: %#", [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath: [NSIndexPath indexPathForRow:0 inSection:0]]);
The output is
"LayoutAttribute: (null)"
even though the layout has all information required to give me the correct layout attributes.
It still does not work in viewWillAppear:, but it "magically" starts working in viewDidAppear:
"LayoutAttribute: <UICollectionViewLayoutAttributes: 0x8c4f710> index path: (<NSIndexPath 0x8c48be0> 2 indexes [0, 0]); frame = (0 0; 50 50);"
I discovered a very strange way to make it work; I just access the collectionViewContentSize before requesting the layout attributes:
NSLog(#"Layout content size: %#", NSStringFromCGSize(self.collectionView.collectionViewLayout.collectionViewContentSize));
NSLog(#"LayoutAttribute: %#", [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath: [NSIndexPath indexPathForRow:0 inSection:0]]);
The output is, as expected all along:
"Layout content size: {768, 50}"
"LayoutAttribute: <UICollectionViewLayoutAttributes: 0x8c4f710> index path: (<NSIndexPath 0x8c48be0> 2 indexes [0, 0]); frame = (0 0; 50 50);"
What is going on here? I assume accessing the content size triggers something in the layout, but does anyone know if there is a proper way to do this? Or is this just an annoying bug?
The documentation for layoutAttributesForItemAtIndexPath: says that it only returns nil if there is no item at that index path. Did you perhaps not call reloadData on your collection view before calling layoutAttributesForItemAtIndexPath:?
Another common problem is that some intermediate object in your call chain is nil. Perhaps collectionView or collectionView.collectionViewLayout is nil?

Assertion Failure in UICollectionViewData indexPathForItemAtGlobalIndex

I am using performBatchUpdates() to update my collection view, where I am doing a complete refresh, i.e. delete whatever was in it and re-insert everything. The batch updates are done as part of an Observer which is attached to a NSMutableArray (bingDataItems).
cellItems is the array containing items that are or will be inserted into the collection view.
Here is the code:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
cultARunner *_cultARunner = [cultARunner getInstance];
if ( [[_cultARunner bingDataItems] count] ) {
[self.collectionView reloadData];
[[self collectionView] performBatchUpdates: ^{
int itemSize = [cellItems count];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
// first delete the old stuff
if (itemSize == 0) {
[arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
}
else {
for( int i = 0; i < cellItems.count; i++ ) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
}
[cellItems removeAllObjects];
if(itemSize) {
[self.collectionView deleteItemsAtIndexPaths:arrayWithIndexPaths];
}
// insert the new stuff
arrayWithIndexPaths = [NSMutableArray array];
cellItems = [_cultARunner bingDataItems];
if ([cellItems count] == 0) {
[arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
}
else {
for( int i = 0; i < [cellItems count]; i++ ) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
}
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}
}
I get this error, but not all of the times (why ?)
2012-12-16 13:17:59.789 [16807:19703] *** Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442
2012-12-16 13:17:59.790 [16807:19703] DEBUG: request for index path for global index 1342177227 when there are only 53 items in the collection view
I checked the only thread that mentioned the same problem here: UICollectionView Assertion failure, but it is not very clear i.e. doing [collectionview reloadData] is not advisable in the performBatchUpdates() block.
Any suggestions on what might be going wrong here ?
Finally! Ok, here's what was causing this crash for me.
As previously noted, I was creating supplementary views in order to provide custom-styled section headers for my collection view.
The problem is this: it appears that the indexPath of a supplementary view MUST correspond to the indexPath of an extant cell in the collection. If the supplementary view's index path has no corresponding ordinary cell, the application will crash. I believe that the collection view attempts to retrieve information for a supplementary view's cell for some reason during the update procedure. It crashes when it cannot find one.
Hopefully this will solve your problem too!
This is the proper workaround to this crash:
Each of your supplementary views are associated with a certain index path. If you don't have a cell at that index path (initial load, you've deleted the row, etc), return a height of 0 for your supplementary view via your layout's delegate.
So, for a flow layout, implement UICollectionViewDelegateFlowLayout's
(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
method (and the corresponding footer method, if you're using footers) with the following logic
if ( you-have-a-cell-at-the-row-for-this-section )
return myNormalHeaderSize;
else return CGSizeMake( 0,0 );
Hope this helps!
reloadData doesn't work for me, because the whole purpose of using performBatchUpdates is to get the changes animated. If you use reloadData you only refresh the data, but without animations.
So suggestions of "replace performBatchUpdates with reloadData" is pretty much saying "give up on what you're trying to do."
I'm sorry, I'm just frustrated because this error keeps coming up for me while I'm trying to do some great animated updates and my model is 100 % correct, it's some iOS magic inside getting broken and forcing me to change my solutions completely.
My opinion is that Collection Views are still buggy and can't do complicated animated refreshes, even though they should be able to. Because this used to be the same thing for Table Views but those are now pretty stable (it took time, though).
//Edit (Sep 1, 2013)
The reported bug is closed now so this issues seems to have been resolved by Apple already.
I have been having the same problem.
I have tried a number of variations, but the final one that seems to work is [self.collectionView reloadData], where "self.collectionView"is the name of your collection view.
I have tried the following methods, straight from the "UICollectionView Class Reference": inserting, moving, and deleting items.
These were used at first, to "move" the item from one section to another.
deleteItemsAtIndexPaths:
insertItemsAtIndexPaths:
Next, I tried moveItemAtIndexPath:toIndexPath:.
They all produced the following error:
Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442
So, try the "reloadData" method.
If you remove the last cell from a section containing header/footer the bug appears.
I tried to return nil for header/footer size/element at that time and this sometimes fixes the issue.
Options:
Reload the whole table view instead of animating the removal of the last item.
Add an additional invisible, basic cell with a size less than 1.
A cheeseball mistake that can lead to this error is reusing the same UICollectionViewFlowLayout on multiple collectionViews on the same viewcontroller! Just init different flowLayouts for each collectionview and you'll be good to go!
I ran into this problem when I delete one of the cells from my collection view.
The problem was that I use a custom layout, and the call layoutAttributesForElementsInRect was returning more than the number of cells in the collection view after the delete.
Apparently UICollectionView just iterates through the array returned by the method without checking the number of cells.
Modifying the method to return the same number of layout attributes solved the crash.
I still couldn't figure out how the global index was incremented so much, but I solved my problem by inserting a temporary item in the underlying datasource array i.e. cellItems and calling [self.collectionview reloadData] in viewDidLoad().
This inserts a placeholder cell temporarily in the collection view until I trigger the actual process using performBatchUpdates().

NSMutablearray's last element gets corrupted and becomes standard NSObject from custom object

I run into a weird problem with a NSMutableArray today.
I'm parsing an XML file and I add the parsed items as custom objects. There are 37 items in total.
So, when my view loads, I did this, as a test:
[parser loadDataBase];
ProductItem* item = [parser.productDetail.prodItems objectAtIndex:36];
NSLog(#"test 1 %#", item.idItem);
self.product = parser.productDetail;
item = [self.product.prodItems objectAtIndex:36];
NSLog(#"test 2 %#", item.idItem);
[parser release];
At this point, everything works just fine. Both NSLog print the correct value for the last item in the mutable array.
The problem is when I try to add these items into a table.
When the app tries to get the item at index 36, to display its properties, instead of a ProductItem custom object, it gets a NSObject object... everything is lost for the last item, being replaced with a mere NSObject.
I do absolutely nothing with the array in that class, or any other class, except the parser.
Everything is ok when it leaves the parser, everything is ok when I read it from the parser, everything is ok when I check to see if I got all the values correctly from the parser. But somehow, the last value gets corrupted after this, even though I don't do anything that might cause this.
Here's the code I use in the cellForRow:
NSLog(#"index %i", indexPath.row);
ProductItem* item = [self.product.prodItems objectAtIndex:indexPath.row];
cell2.itemName.text = item.name;
The row is 36, the last one, and boom! EXC_BAD_ACCESS when I try to read the name property.
Does anyone here have a clue about what might be happening? I never ran into such a problem before
Thank you for your time and attention!
With some help from a colleague, I found out what was happening.
I was releasing my currentParsedItem in the parser's dealloc, so everything looked ok before the [parser release]; but after that the, "currentParsedItem" that was the last object in my mutable array was being set to nil in the dealloc function of the parser, and I ended up with a blank NSObject
Hope this tip helps others with a similar problem!

reloadData in NSTableView but keep current selection

I have anNSTableView showing the contents of a directory. I watch for FSEvents, and each time I get an event I reload my table view.
Unfortunately, the current selection then disappears. Is there a way to avoid that?
Well, you can save selection before calling reloadData and restore it after that.
NSInteger row = [self.tableView selectedRow];
[self.tableView reloadData];
[self.tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
This worked for me in some cases. But if you insert some items BEFORE the selected row, you should andjust your row variable by adding count of added items to it.
Swift 4.2
Create an extension and add a method which preserves selection.
extension NSTableView {
func reloadDataKeepingSelection() {
let selectedRowIndexes = self.selectedRowIndexes
self.reloadData()
self.selectRowIndexes(selectedRowIndexes, byExtendingSelection: false)
}
}
Do this in case you use the traditional way of populating table views (not NSArrayController).
It depends on how you populate your NSTableView.
If you have the table view bound to an NSArrayController, which in turn contain the items that your table view is displaying, then the NSArrayController has an option to preserve the selection. You can select it (or not) from within Interface Builder as a property on the NSArrayController. Or you can use the setPreservesSelection method in code.
However, if you completely replace the array of items that the NSArrayController manages each time you get your FSEvents, then maybe the preservation of selection cannot work. Unfortunately the Apple docs on this property of NSArrayController are a bit vague as to when it can and cannot preserve the selection.
If you are not using an NSArrayController, but maybe using a dataSource to populate the table view, then I think you'll have to manage the selection yourself.
In the case of using Data Source, Apple Documentation in the header file on reloadData() is that
The selected rows are not maintained.
To get around, you can use reloadDataForRowIndexes(rowIndexes: NSIndexSet, columnIndexes: NSIndexSet). As mentioned in the same header file
For cells that are visible, appropriate dataSource and delegate methods will be called and the cells will be redrawn.
Thus the data will be reloaded, and the selection is kept as well.
A variant on #silvansky's answer.
This one has no need to keep track of count of items inserted/deleted. And it maintains multiple selection.
The idea is to...
1. create an array of selected objects/nodes from the current selection.
2. refresh the table using reloadData
3. for each object obtained in step 1, find/record it's new index
4. tell the table view/outline view to select the updated index set
- (void)refresh {
// initialize some variables
NSIndexSet *selectedIndexes = [self.outlineView selectedRowIndexes];
NSMutableArray *selectedNodes = [NSMutableArray array];
NSMutableIndexSet *updatedSelectedIndex = [NSMutableIndexSet indexSet];
// 1. enumerate all selected indexes and record the nodes/objects
[selectedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[selectedNodes addObject:[self.outlineView itemAtRow:idx]];
}];
// 2. refresh the table which may add new objects/nodes
[self.outlineView reloadData];
// 3. for each node in step 1, find the new indexes
for (id selectedNode in selectedNodes) {
[updatedSelectedIndex addIndex:[self.outlineView rowForItem:selectedNode]];
}
// 4. tell the outline view to select the updated index set
[self.outlineView selectRowIndexes:updatedSelectedIndex byExtendingSelection:NO];
}