UICollectionView Repeats and Reorders Cell Content When Refreshed - ios

I'm trying to work through the iTunes U CS193P iOS programming course. I'm running into some issues using UICollectionView to display cards as a part of the Set game implementation. I'm seeing a very odd behavior when clicking on a card causing the UICollectionView to refresh. Contents will be reordered and sometimes repeated in the view. See two examples of before and afters here.
I'm fairly certain this has to do with my UICollectionView implementation as the game logic has been tested using buttons.
Tap code in the "view controller":
- (IBAction)touchCard:(UITapGestureRecognizer *)sender {
CGPoint tapLocation = [sender locationInView:self.cardCollectionView];
NSIndexPath *indexPath = [self.cardCollectionView indexPathForItemAtPoint:tapLocation];
if (indexPath) {
[self.game chooseCardatIndex:indexPath.item];
[self updateUI];
}
}
The updateUI code:
- (void)updateUI
{
// Reload data for cardCollectionView
[self.cardCollectionView reloadData];
// For each cell within the UICollectionViewCell
for (UICollectionViewCell *cell in [self.cardCollectionView visibleCells]) {
NSIndexPath *indexPath = [self.cardCollectionView indexPathForCell:cell];
Card *card = [self.game cardAtIndex:indexPath.item];
if([card isKindOfClass:[SetCard class]]) {
SetCard *setcard = (SetCard *)card;
NSLog([NSString stringWithFormat:#"index %d updated with %d,%d,%d,%d",indexPath.item,setcard.rank,setcard.color,setcard.shape,setcard.pattern]);
// Updates individual cards for each cardCollectionView
[self updateCell:cell usingCard:card animated:YES];
}
}
}
This updates the cell view:
- (void)updateCell:(UICollectionViewCell *)cell usingCard:(Card *)card animated:(BOOL)animated
{
setCardView *cardView = ((setCardCellView *)cell).setCardV;
if([card isKindOfClass:[SetCard class]]) {
SetCard *setCard = (SetCard *)card;
// Set attributes of the card in the cardView
cardView.rank = setCard.rank;
cardView.shape = setCard.shape;
cardView.pattern = setCard.pattern;
cardView.color = setCard.color;
// Set chosen cards
cardView.selected = setCard.isChosen;
}
}
Finally the collectionView required method cellForItemAtIndexPath:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[self reuseIdentifier] forIndexPath:indexPath];
Card *card = [self.game cardAtIndex:indexPath.item];
[self updateCell:cell usingCard:card animated:YES];
return cell;
}
Thank you for your assistance.

When you implement a cellForRowAtIndexPath / cellForItemAtIndexPath method, you have to fully configure every cell that you get back from the dequeue method.
Your code is only configuring the cell if the card object for that row is of type SetCard. If the card object is not a SetCard, you leave the cell alone. That means that if the cell you get from your dequeue method was previously used to display a SetCard, but the card object is not a SetCard, the cell's views won't get changed, and will still show the old values.
Add an else clause to your if statement that sets the fields back to their default state if the card object is not of class SetCard.

Related

Get UICollectionViewCell from indexPath when UIBarButtonItem is tapped

I have created a View Controller with a Navigation bar and UiCollectionView. UI Collection View contains custom UICollectionViewCell. Navigation bar contains two UIBarButton items, one is on the left corner - prepared segue to previous page and other item is on the right corner - arranged to delete cell(s) in the UI CollectionView as show in the picture below:
Main Screen
Now I want to remove the selected UICollectionViewCell when UIBarButtonItem in the right corner, is tapped.
This how my cellForItemAtIndexPath method look like:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath{
self.GlobalIndexPath = indexPath;
MessagesCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"messagesCell" forIndexPath:indexPath];
cell.MessageHeading.text = [self.Message_Heading objectAtIndex:indexPath.row];
cell.MessageSubject.text = [self.Message_Subject objectAtIndex:indexPath.row];
cell.MessageContent.text = [self.Message_Details objectAtIndex:indexPath.row];
[cell.Checkbox setHidden:YES];
[cell.Checkbox setChecked:NO];
}
I have tried a solution like Declaring Indexpath as global variable and use it in the button event as below:
#property (strong,nonatomic) NSIndexPath *GlobalIndexPath;
some other code .......
//When Bin Icon(UIBarButtonItem) Clicked
- (IBAction)DeleteMessages:(id)sender {
[self.view makeToast:#"You clicked delete button !"];
NSIndexPath *indexPath = [self.MessageCollectionView.indexPathsForVisibleItems objectAtIndex:0] ;
BOOL created = YES;
// how to get desired selected cell here to delete
MessagesCollectionViewCell *cell = [self.MessageCollectionView cellForItemAtIndexPath:self.GlobalIndexPath];
if([cell.Checkbox isHidden])
{
[cell setHidden:YES];
}
else{
[cell.Checkbox setChecked:NO];
[cell.Checkbox setHidden:YES];
}
}
It's not worked.
For showing the UICollectionViewCell selected as checked, i'm using #Chris Chris Vasselli's solution
Please help me with this. Thanks in Advance.
There are a few steps. First, determine the selected indexPath, but don't assume there is a selection when the method is run....
// in your button method
NSArray *selection = [self.MessageCollectionView indexPathsForSelectedItems];
if (selection.count) {
NSIndexPath *indexPath = selection[0];
[self removeItemAtIndexPath:indexPath];
}
There are two more steps to remove items from a collection view: remove them from your datasource, and tell the view it has changed.
- (void)removeItemAtIndexPath:(NSIndexPath *)indexPath {
// if your arrays are mutable...
[self.Message_Heading removeObjectAtIndex:indexPath.row];
// OR, if the arrays are immutable
NSMutableArray *tempMsgHeading = [self.Message_Heading mutableCopy];
[tempMsgHeading removeObjectAtIndex:indexPath.row];
self.Message_Heading = tempMsgHeading;
// ...
Do one or the other above for each datasource array. The last step is to inform the collection view that the datasource has changed, and it must update itself. There are a few ways to do this. The simplest is:
// ...
[self.MessageCollectionView reloadData];
OR, a little more elegantly:
[self.MessageCollectionView deleteItemsAtIndexPaths:#[indexPath]];
} // end of removeItemAtIndexPath

How to differenciate indexPath of first cell with indexPath of empty space at the end of table?

I use the following code to detect a click on a UITableView and take action depending on which cell is clicked, and which element in the cell was clicked, with a default action for any element that doesn't match.
-(void)addTapRecognizer {
// this is called when the view is created
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
singleTap.delegate = self;
singleTap.numberOfTapsRequired = 1;
singleTap.numberOfTouchesRequired = 1;
[self.tableView addGestureRecognizer:singleTap];
}
- (void)handleTap:(UITapGestureRecognizer *)tap {
NSLog(#"tap detected!");
if (UIGestureRecognizerStateEnded != tap.state) {
return;
}
UITableView *tableView = (UITableView *)tap.view;
CGPoint p = [tap locationInView:tap.view];
NSIndexPath* indexPath = [tableView indexPathForRowAtPoint:p];
[tableView deselectRowAtIndexPath:indexPath animated:NO];
NSLog(#"selectedIndex = %ld", (long)indexPath.row);
// take action depending where the cell was clicked
// with a default action if no element matches
MyTableViewCell *cell = (MyTableViewCell *) [self.tableView cellForRowAtIndexPath:indexPath];
CGPoint pointInCell = [tap locationInView:cell];
if(CGRectContainsPoint(cell.someImage.frame,pointInCell)) {
[self openItemID:[ItemList[indexPath.row] valueForKey:ID_ITEM]];
return;
}
if (...) {
...
return;
}
[self openItemID:[ItemList[indexPath.row] valueForKey:ID_ITEM]];
return;
}
My problem is that when there aren't enough cells to fill the screen (so for instance the table contains 2 cells and then blank space below), when the user clicks below the last cell, this is treated as a click on the first cell (the console logs "selectedIndex = 0" in both cases).
Is there a way to tell the difference between such a click, in the empty space at the end of the table, and a click on a "proper" cell of the table?
Is there a way to tell the difference between such a click, in the empty space at the end of the table, and a click on a "proper" cell of the table?
Yes. For the cheap and easy solution only do what you are trying to do if you actually get an indexPath:
NSIndexPath *indexPath = [tableView indexPathForRowAtPoint:p];
if(indexPath != nil){
// do everything in here
}
Basically, your indexPath is returning nil because it can't find a row. From the docs:
An index path representing the row and section associated with point, or nil if the point is out of the bounds of any row.
You could do it the way that you're currently doing it but is there any reason why you aren't using:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
This is a much more standard way of detecting the cell that the user tapped on.
This method won't be called if you tap on something that isn't a cell and has a number of other benefits. Firstly you get a reference to the tableView and the indexPath for free. But you also won't need any gesture recognisers this way either.
Try something like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Do stuff with cell here...
}
Obviously this all assumes that you have correctly set a class as your table view's delegate.
NB: It's very easy to mistakenly write didDeselectRowAtIndexPath instead of didSelectRowAtIndexPath when using Xcode's autocompletion to do this. I always do this and then inevitably realise my mistake 20 minutes later.

How can I update a tableview cell from a -(void) method?

I am trying to update a specific dynamic cells font size from a -(void) method or some way without using cellForRowAtIndexPath. Is this possible?
Thanks
If you know the position of the index, as follows:
First create indexPath by row at position:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
Then access to cell:
MyCell *cell = (MyCell *)[self.tableView cellForRowAtIndexPath:indexPath];
Call
[self.view.tableView reloadRowsAtIndexPaths:#[your_index_path] withRowAnimation:UITableViewRowAnimationAutomatic];
inside your method. that will cause row update
You can not update a cell without having a call to: cellForRowAtIndexPath.
When you want to update font size in a cell, then reload the cell. In your cellForRowAtIndexPath method add a conditional statement to check the indexPath of the cell you want a different font size and edit different font size. Do not forget to add else condition so that when the cell reloads it will make the font size normal for other cells.
However if you want somehow. Then you can use global reference variable to the cell. But this will only work if the cell is displaying on screen.
Here is the example code:
#interface TableViewController ()
{
UITableViewCell *aCell;
}
#end
#implementation TableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *theCell = [tableView dequeueReusableCellWithIdentifier:#"TableViewCellIdentifier" forIndexPath:indexPath];
if(indexPath.row == SPECIFIC_ROW_NUMBER){
aCell = theCell;
theCell.tag = SPECIFIC_ROW_NUMBER;
}else if (theCell.tag == SPECIFIC_ROW_NUMBER){
// If the cell is dequeued
aCell = nil;
theCell.tag = 99999; // some unused row number
}
}
- (void)someMethod {
if(theCell != nil){
aCell.titleLabel.font = NEW_FONT;
}
}
#end

Selecting different UITableViewCells from indexPath alone

I have a scenario where upon selecting a UITableViewCell in didSelectRowAtIndexPath:, I need to load and get the information from a different UITableViewCell.
I'm registering and using two different xibs to be used as my tableViewCells to allow for some more customization.
- (void)viewDidLoad{
[super viewDidLoad];
self.TABLE_ROW_HEIGHT = 66;
self.tblView.delegate = self;
self.tblView.dataSource = self;
[self.tblView registerNib:[UINib nibWithNibName:#"BasicCell" bundle:nil] forCellReuseIdentifier:#"BasicCell"];
[self.tblView registerNib:[UINib nibWithNibName:#"DetailCell" bundle:nil] forCellReuseIdentifier:#"DetailCell"];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//Property of the view controller which is an IndexPath
self.selectedIndex = indexPath;
BasicModel *basicModel = [self.models objectAtIndex:indexPath.row];
[self.apiClient detailModelSearch:basicModel.id];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if([self.selectedIndex isEqual:indexPath]){
return 400.0f;
}
return 66.0f;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
BasicModel *basicModel = [self.models objectAtIndex:indexPath.row];
UITableViewCell *tableCell = nil;
if([self.selectedIndex isEqual:indexPath]){
DetailCell *detailCell = [tableView dequeueReusableCellWithIdentifier:#"DetailCell"];
tableCell = detailCell;
}
else{
BasicCell *basicCell = [tableView dequeueReusableCellWithIdentifier:#"BasicCell"];
tableCell = basicCell;
}
return tableCell;
}
-(APIClient *)apiClient{
if(!_apiClient){
_apiClient = [APIClient new];
__weak ViewController *_self = self;
_apiClient.detailModelSearchFinished = ^(DetailModel *detailModel){
_self.detailModel = detailModel;
//Problem is here
DetailCell *cell = [_self.tblView cellForRowAtIndexPath:_self.selectedIndexPath;
dispatch_async(dispatch_get_main_queue(), ^{
[_self.tblView beginUpdates];
[_self.tblView endUpdates];
[_self.tblView reloadData];
});
};
}
return _apiClient;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.models.count;
}
The basic structure is as follows.
App load and loads all BasicModels into the the models array.
User selects a cell which prompts an API detail search request
When detail search request is finished, the callback returns a DetailModel
What should happen next is since I know the selected index path of the touched cell, I want to use the DetailCell instead of the BasicCell to present the detailedInformation that comes from the DetailModel. My problem is when I call
DetailCell *cell = [_self.tblView cellForRowAtIndexPath:_self.selectedIndexPath;
I always receive the BasicCell that does not have the detailed view components I need to bind the detailModel to.
BasicCell xib
Detail Cell Xib
Table View Normal:
Table View Expanded with detail Cel xib
Ok now is very clear.
I can think of two ways, one is if you don't care about fancy animations just remove the (basic) cell and insert a new (detail) cell at the same index path, only after tho you have updated the model as well and perform the eventual type checks.
Quick and dirty, if you want something more clean you may want to refactor the model objects using polimorfism or other suitable patterns.
Another way is to update directly the cell with the received data. You may apply some fancy animations but loosing possibly some performances advantages.
Pretty simple solution actually. Reload data must be called before I can grab the expanded cell. Simple as this:
[_self.tblView beginUpdates];
[_self.tblView endUpdates];
[_self.tblView reloadData];
DetailCell *expandedCell = (DetailCell *) [_self.tblView cellForRowAtIndexPath:_self.selectedIndex];
expandedCell.lblData.text = #"IT WORKS!";
});

Weird behaviour using 2 cell dentifiers in a tableViewController

I'm playing with the cell identifier in a TableviewController, I'm using storyBoard with dynamic cells.
I set 2 cells with 2 identifiers and used a custom row height:
In my viewDidLoad, I just insert some checking information to a mutableArray:
- (void)viewDidLoad
{
[super viewDidLoad];
_arrayCellContent = [[NSMutableArray alloc]init];
for (int i=0; i<15; i++) {
if (i%2 == 0) {
[_arrayCellContent addObject:#"white"];
}
else
[_arrayCellContent addObject:#"Brown"];
}
}
My cellForRow delegate method is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* identifierCellSlim = #"cellSlim";
static NSString* identifierCellFat = #"cellFat";
UITableViewCell *cellSlim = [tableView dequeueReusableCellWithIdentifier:identifierCellSlim forIndexPath:indexPath];
UITableViewCell *cellFat = [tableView dequeueReusableCellWithIdentifier:identifierCellFat forIndexPath:indexPath];
if (indexPath.row%2 ==0) {
cellSlim.textLabel.text = self.arrayCellContent[indexPath.row];
return cellSlim;
}
// Configure the cell...
else {
cellFat.textLabel.text = self.arrayCellContent[indexPath.row];
cellFat.detailTextLabel.text = #"yalla";
return cellFat;
}
}
The final outcome is:
Even cell row heights, not costumed. Why is that? (I know I might work it out with the right delegate method, but I just want to know why the IB not doing his job)
Also in the beginning the cells appear to be in the right color but when I click some of them they will transform to the other cell,
Does clicking the cell change the indexPath ?
Example:
You are dequeuing both cells in the same method call.
You should check for which cell is necessary for the given NSIndexPath and dequeue the relevant cell.

Resources