collectionView:cellForItemAtIndexPath: doesn't get called - objective-c

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"

Related

Updating NSTableView datasource asynchronously

I have been googling on this subject, but didn't seem able to find a consensus on the solution to this type of problem. When I use a data source with an NSTableView, if I need to populate the data source in background, there're a couple questions that pop in my mind regarding threading. I'm hoping to get some guidance here.
What would happen if I modified the data source between the main threading calling [NSTableView numberOfRowsInTableView:] and [NStableView tableView:objectValueForTableColumn:row:]? If the object the table view is asking for isn't valid anymore, what should I do?
Is making change to data source only on main thread the solution to this situation?
If 2 is the answer, does it apply to the case when binding is used?
If your data source takes some time to populate, and you're currently showing a table with older data, I think you have a couple of options:
Show a spinner over the UI while the re-population occurs, then call [tableView reloadData]
Keep the older data around so the tableView remains responsive, then once the new data has been fetched/computed, tell the datasource about the new NSArray (or whatever object holds the new data), and call [tableView reloadData].
You can't be changing the data backing your datasource on the fly, unless you inform the tableView of each item/row changing as you go.
To address the threading part, you can use a background thread to populate an NSArray of new data, once complete switch to the main thread, and on that call [dataSource setBackingArray:newStuff]; [tableView reloadData];
Many thanks to Graham Perks in comments to one of answers. This actually an answer worth to be written out explicitly. I want just to add small snippet from my project as illustration:
- (void) populateTable
{
DAL *dal = [[DAL alloc] init]; // MySQL engine
NSMutableArray *tmp = [NSMutableArray new];
NSMutableArray *records = [dal RetrieveRecordswithSql:#"select id, serial, scannerid, scans, offloaded, uploaded from scan_set_v3" withColumnsCount:#(6) andColumnsDelimiter:ScanSetRecordColumnDelimiter];
for (NSString *rec in records) {
ScanSetRecord *newRec = [[ScanSetRecord alloc] initWithScanSet:rec];
if (newRec) {
[tmp addObject:newRec];
}
}
self.dataArray = tmp;
[self.tableView reloadData];
}

Observe changes of instance variable in Objective-c

I have a UITableView with a property(strong, nonatomic)NSMutableArray *currentContent, I also have a property(strong, nonatomic)NSMutableArray *cellHeights to keep track of the cell heights cos the user could expand or collapse each cell. The self.currentContent is set by another controller which load data from a web service, so it will change as the data load, I want to keep both of these variables in sync. As soon as currentContent is updated, I want to update cellHeights. How do I do that?
I tried:
- (void)setCurrentContent:(NSMutableArray *)currentContent{
_currentContent = currentContent;
self.cellHeights = [NSMutableArray arrayWithDefaultHeightsForCellCount:[currentContent count]];
}
But it's not working, cos it will only be set at the first time when I set currentContent, when it's empty. So self.cellHeights currently will stay empty. When there is finally value in self.currentContent, self.cellHeights was not updated.
I've done a similar thing before, with variable cell heights depending on the content from your web-service, and I'd advise that keeping an array of cell heights might not be the best idea.
What I did was to create a 'fake' cell in the viewDidLoad: method, that I use just to calculate cell heights.
Then I use the 'heightForRowAtIndexPath' method to specify how tall cell should be by populating the 'fake' cell with the data for the index path, then finding out how tall that cell is. For example:
#interface MyTableViewController()
#property (nonatomic, strong ) MyCustomTableViewCell *cellForTestingHeight;
#end
#implementation MyTableViewController
- (void)viewDidLoad {
[super viewDidLoad]
self.cellForTestingHeight = [[MyCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *myData = [self.data objectAtIndex:indexPath.row];
self.cellForTestingHeight.viewData = myData;
return self.cellForTestingHeight.height;
}
#end
This code assumes that you've created a class called MyCustomTableViewCell which has a method to set the viewData property on it, and that after setting that property you'll be able to tell how tall that cell will be by accessing a height property.
What you need to do is observe changes to the mutable array itself, not your property which references the array. This should be straightforward but unfortunately there is a twist...
OK, let's assume there is no twist and rather than an NSMutableArray your property is of type MyObservableClass. Then to setup the observing you would do something like this (all code is pseudo - i.e. typed into the answer):
- (void)setCurrentContent:(MyObservableClass *)currentContent
{
if(_currentContent)
[_currentContent removeObserver:self forKeyPath:#"myObservableProperty"];
_currentContent = currentContent;
[_currentContent addObserver:self forKeyPath:#"myObservableProperty" ...];
}
Now whenever the currentContent is changed your code stops observing the properties of its old value and starts observing properties of its new value.
Easy, but its not quite that simple for arrays...
The twist, though KVO will inform an observer of additions, deletions and changes to a collection the NSMutableArray class doesn't issue those notifications. You need to use a proxy for the array, and such a proxy is provided by the frameworks - see for example NSArrayController and mutableArrayValueForKey:. However the second part of the twist is that the changes to the array must be done through this proxy - you can't have your client call setCurrentContent and pass any NSMutableArray if you are going to observe it, your client needs to pass in the proxy... Whether you can use this easily in your design you'll have to figure out. You'll need to read Automatic Change Notification in this document and other Apple docs to sort this out.
This sounds more complicated that it needs to be, maybe somebody else will provide a more succinct way of doing this.

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().

indexPath Value of UICollectionView

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.

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!