I've been trying to wrap my head around the reloadItemsAtIndexPaths method of UICollectionView.
I have an array of objects called objectsArray. When a user scrolls to the bottom of my collection view, I fetch the next batch of objects from the backend and append it to objectsArray by calling [objectsArray addObjectsFromArray:objects]; After doing so, I call [self.collectionView reloadData] which I know is expensive.
I'd like to optimize with the code below but I get an assertion failure when calling reloadItemsAtIndexPaths.
if (self.searchPage == 0) {
parseObjectsArray = [[NSMutableArray alloc] initWithArray:relevantDeals];
[self.collectionView reloadData];
} else {
NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
for (int i = 0; i < [relevantDeals count]; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
[indexPaths addObject:indexPath];
}
[parseObjectsArray addObjectsFromArray:relevantDeals];
[self.collectionView reloadItemsAtIndexPaths:indexPaths];
//[self.collectionView reloadData];
}
Error:
* Assertion failure in -[UICollectionView _endItemAnimations], /SourceCache/UIKit/UIKit-2903.2/UICollectionView.m:3716
Any help/tips is greatly appreciated.
Thanks!
It looks like the items being added are new, in other words you are adding items which weren't there before. In that case, just replace reloadItemsAtIndexPaths:indexPaths with insertItemsAtIndexPaths:
[self.collectionView insertItemsAtIndexPaths:indexPaths]; // Use this for newly added rows
//[self.collectionView reloadItemsAtIndexPaths:indexPaths]; // Use this for existing rows which have changed
The assertion error was misleading. It wasn't until after I implemented
#try
{
[self.collectionView insertItemsAtIndexPaths:indexPaths];
}
#catch (NSException *except)
{
NSLog(#"DEBUG: failure to insertItemsAtIndexPaths. %#", except.description);
}
Did I realize was calculating my indexPaths wrong. My index offset for the new objects was off by one making my collectionview think I was adding more objects than I specified in numberOfItemsInSection.
Thanks for all those who attempted to help!
This error often arises when you increase/decrease the number of items in the array but it doesn't match the data source method
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
Are you updating it appropriately?
Related
EDIT: I should specify that this only happens when I attempt to use the UICollectionViewFlowLayout, not when I try to use a custom view. But either way nothing ever shows up on the CollectionView though it was working just fine before I converted from a TableView.)
So I've been trying to convert a UITableView that I had into a UICollectionView. So far so good. But when I try to run the app it gives me the error:
'the collection view's data source did not return a valid cell from -collectionView:cellForItemAtIndexPath: for index path {length = 2, path = 0 - 0}'
I checked all the similar questions and answers here... so in my viewDidLoad I have (tableView is actually a UICollectionView):
UINib * placeCell = [UINib nibWithNibName:#"Shops" bundle:nil];
[self.tableView registerNib:placeCell
forCellWithReuseIdentifier:CellIdentifier];
#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UICollectionView *)tableView numberOfItemsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [_entries count];
//return 5;
}
- (void)tableView:(UICollectionView *)tableView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item == [_entries count]-1 && page > 1) {
NSLog(#"load more");
//add footer view loading
if (c_page == page) {
// _tableView.tableFooterView = nil;
}
else
{
c_page++;
[self loadPlace:c_page];
}
}
}
- (UICollectionViewCell *)tableView:(UICollectionView *)tableView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
PlaceCell *cell = (PlaceCell *)[tableView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
UINib * placeCell = [UINib nibWithNibName:#"Shops" bundle:nil];
//cell = [cellLoader instantiateWithOwner:self options:nil];
NSArray *topLevelItems = [placeCell instantiateWithOwner:self options:nil];
cell = [topLevelItems objectAtIndex:0];
Place *p = [_entries objectAtIndex:indexPath.row];
cell.placeName.text = p.PName;
NSLog (#"p:%#", p.PName")
cell.placeImg.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:p.PImage]]];
return cell;
}
I went into the xib of the UICollectionViewCell (PlaceCell) and made sure that "Cell" was the reuseidentifier. And I made sure that the datasource and delegate were connected to file's owner in the collectionView.
I also noticed that when I use a custom layout instead of the flow layout (like this one: https://github.com/ShadoFlameX/PhotoCollectionView/blob/master/CollectionViewTutorial/BHPhotoAlbumLayout.m ) it doesn't give me that error... but my collectionview still isn't populated.
So I'm wondering if there's some sort of log I can run or something I can do to figure out what's going wrong. Because I've tried all the solutions I've seen and it hasn't gotten me anywhere.
When you make a cell in a xib file you should register the xib, not the class. Also, when you register either the class or xib (or make the cell in the storyboard), you don't need an if (cell==nil) clause because your cell will never be nil when you dequeue it with dequeueReusableCellWithReuseIdentifier:forIndexPath:. You should delete that clause.
So the problem is: "Switched from UITableView to UICollectionView and no valid cell is being returned." It is really a two part answer. The crux of which is every instance of UITableView...
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 50, self.view.bounds.size.width, self.view.bounds.size.height-50)];
...you want to turn into "CollectionView"
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 50, self.view.bounds.size.width, self.view.bounds.size.height-50)];
Everything that's a "row":
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
...you'll want to turn into an "item."
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
Ultimately I had to delete the following section entirely:
UINib * placeCell = [UINib nibWithNibName:#"Shops" bundle:nil];
//cell = [cellLoader instantiateWithOwner:self options:nil];
NSArray *topLevelItems = [placeCell instantiateWithOwner:self options:nil];
cell = [topLevelItems objectAtIndex:0];
My best guess is that the Nib was being loaded twice and that Xcode was complaining that the data wasn't being loaded by the original. So getting rid of that second entry got my cells loaded and populated with data. Hope this helps someone.
I have a query that grabs 10 objects, and I am trying to have an action that shows you a window with more information on the object selected, I am just new to Xcode and cannot figure out how to push the information to the new view controller. Here is the code I have that creates the query.
PFQuery *query = [PFQuery queryWithClassName:#"Arcade"];
CLLocation *currentLocation = locationManager.location;
PFGeoPoint *userLocation =
[PFGeoPoint geoPointWithLatitude:currentLocation.coordinate.latitude
longitude:currentLocation.coordinate.longitude];
query.limit = 10;
[query whereKey:kPAWParseLocationKey nearGeoPoint:userLocation withinMiles:kPAWWallPostMaximumSearchDistance];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %d scores.", objects.count);
// Do something with the found objects
int i = 0;
for (PFObject *object in objects) {
if (i >= [self.EventTitles count]) break;//to make sure we only write up to the max number of UILabels available in EventTitles
[(UILabel *)self.EventTitles[i] setText:[object objectForKey:#"name"]];//I assume the "objectId" property of object is an NSString!
i++;
}
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
and this is the action to push to the new view controller:
-(IBAction)DetailEvent1:(id)sender{
TableDetailViewController *objDetail = [[TableDetailViewController alloc] initWithNibName:#"TableDetailViewController" bundle:nil];
[self addChildViewController:objDetail];
objDetail.view.frame = CGRectMake(0.0f, 0.0f, 320.0f, self.view.bounds.size.height - 0.0f);
[self.view addSubview:objDetail.view];
}
If someone could help me write the one line of code that needs to be added to the DetailEvent1 action so that the chosen object is pushed I would greatly appreciate it.
TableDetailViewController *objDetail =
[[TableDetailViewController alloc]
initWithNibName:#"TableDetailViewController" bundle:nil];
objDetail.query = self.query
In other words, this is the moment you are creating the next view controller. The two view controllers are now in contact (self and objDetail). So this is the moment to pass data across from the one to the other.
Of course, there is no TableDetailViewController property query. Not yet! But you're going to make one, don't you see - so that you can make this exact move.
Oh, and perhaps there is no query property in self either. But you will need one, because you need a way to hold on to the query that you got in that first method so that it is still available to you in the second method. The way to share data between methods of the same object is very often thru a property.
See also this example from my book:
- (void)showItemsForRow: (NSIndexPath*) indexPath {
// create subtable of tracks and go there
TrackViewController *t =
[[TrackViewController alloc] initWithMediaItemCollection:
(self.albums)[indexPath.row]];
[self.navigationController pushViewController:t animated:YES];
}
In that example, I've gone even further: I've actually given TrackViewController a designated initializer so that I can create it and hand it the data all in one line.
I am trying to create a nested collection view. First I did for one level.
Created a data model class with String header. In app delegate created an array sectionTitle. Now in the nib, I added collection view & array controller and did all the bindings following this guide. Next in awakeFromNib I populated some random data
- (void)awakeFromNib {
int idx = 0;
NSMutableArray *sectionTitle = [[NSMutableArray alloc] init];
while (idx < 1) {
HeaderModel *header = [[HeaderModel alloc] init];
[header setHeader:[NSString stringWithFormat:#"Section %d", idx]];
[sectionTitle addObject:header];
idx++;
}
[self setHeaderData:sectionTitle];
}
Running it will give me 4 sections. I want to achieve similar layout as this. Section title, under it another collection of items. The answer given there only hints at using Nested collection view.
So I added another collection view in the first view prototype. Then I followed the same approach what I did for the first view(with different data model and array).
- (void)awakeFromNib {
int idx = 0;
NSMutableArray *sectionTitle = [[NSMutableArray alloc] init];
NSMutableArray *groupData = [[NSMutableArray alloc] init];
while (idx < 1) {
HeaderModel *header = [[HeaderModel alloc] init];
DataModel *name = [[DataModel alloc] init];
[header setHeader:[NSString stringWithFormat:#"Section %d", idx]];
[name setName:[NSString stringWithFormat:#"Name %d", idx]];
[sectionTitle addObject:header];
[groupData addObject:name];
idx++;
}
[self setHeaderData:sectionTitle];
[self setData:groupData]; //NSCollectionView item prototype must not be nil.
}
But now I get the error NSCollectionView item prototype must not be nil.
How do I resolve this ?
I have just answered a similar question here
But somehow by inserting the second NSCollectionView with I.B, you get a corrupted prototype for your inner NSCollectionViewItem. Simply try to extract each associated NSView into its own .xib
I'm trying to add some more cells to an existing UICollectionView, which is already filled with some cells.
I tried to use the CollectionView reloadData but it seems to reload the entire collectionView and I just wanted to add more cells.
Can anybody help me?
The UICollectionView class has methods to add/remove items. E.g., to insert an item at some index (in section 0), modify your model accordingly and then do:
int indexPath = [NSIndexPath indexPathForItem:index];
NSArray *indexPaths = [NSArray arrayWithObject:indexPath inSection:0];
[collectionView insertItemsAtIndexPaths:indexPaths];
The view will do the rest.
The easiest way to insert new cells to the UICollectionView without having to reload all its cell is by using the performBatchUpdates, which can be done easily by following the steps below.
// Lets assume you have some data coming from a NSURLConnection
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *erro)
{
// Parse the data to Json
NSMutableArray *newJson = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
// Variable used to say at which position you want to add the cells
int index;
// If you want to start adding before the previous content, like new Tweets on twitter
index = 0;
// If you want to start adding after the previous content, like reading older tweets on twitter
index = self.json.count;
// Create the indexes with a loop
NSMutableArray *indexes = [NSMutableArray array];
for (int i = index; i < json.count; i++)
{
[indexes addObject:[NSIndexPath indexPathForItem:i inSection:0]];
}
// Perform the updates
[self.collectionView performBatchUpdates:^{
//Insert the new data to your current data
[self.json addObjectsFromArray:newJson];
//Inser the new cells
[self.collectionView insertItemsAtIndexPaths:indexes];
} completion:nil];
}
I've been struggling with a problem that I hope someone can help me with.
I have Class called 'GameObjectDefinitionTable', where I set all my object properties, which is in another class called 'Product'. In my 'HelloWorldScene' I allocate 'GameObjectDefinitionTable' which in turn creates several 'Product's. Like this:
HelloWorldScene -> GameObjectDefinitionTable -> Product
I then want to return a 'Product' to 'HelloWorldScene. But here is where I get problems. Some code:
HelloWorldScene:
GameObjectDefinitionTable *god = [[GameObjectDefinitionTable alloc]init];
Product* currentProduct = [god getProductWithNum:0];
NSLog(#"currenProduct (name): %#",currentProduct.name); //Crash
GameObjectDefinitionTable:
-(void) createProducts {
Product *product;
for (int i=0; i<[allProductsWithDefinitions count];i++ ) {
product = [[Product alloc]init];
product.name = [[allProductsWithDefinitions objectAtIndex:i] objectAtIndex:0];
product.density = [[[allProductsWithDefinitions objectAtIndex:i] objectAtIndex:1] floatValue];
product.scoreValue = [[[allProductsWithDefinitions objectAtIndex:i] objectAtIndex:2] intValue];
product.fileName = [[allProductsWithDefinitions objectAtIndex:i] objectAtIndex:3];
[products addObject:product];
[product release];
}
[allProductsWithDefinitions release];
}
-(Product *) getProductWithNum:(int)tNum {
Product *tempProduct;
tempProduct = [products objectAtIndex:tNum];
return tempProduct;
[tempProduct release];
}
The arrays and all in 'GameObjectDefinitionTable' is working fine if I log in that class.
Would be really grateful for an answer :)
Is it the case you need something like:
- (Product *)getProductWithNum:(int)tNum
{
Product *tempProduct = [[products objectAtIndex:tNum] retain];
return [tempProduct autorelease];
}
Product *tempProduct;
tempProduct = [products objectAtIndex:tNum];
return tempProduct;
[tempProduct release];
Is that what you mean to have? You kind of have two big problems cancelling each other out here. The [tempProduct release]; line is unreachable. The second, if you were to actually execute [tempProduct release]; before the return, you would be releasing the memory, and then accessing the currentProduct.name property of what is essentially a dangling pointer. This illegal memory access could cause your Bad Exec.
Since you are not allocating, copying, or retaining tempProduct, you must NOT release it. Why not just a simple return [products objectAtIndex:tNum];?