Messing UIButton in each UICollectionViewCell in Scrolling Filmstrip - ios

This is a follow-up question to my previous one and this time I have a problem with UIButton that I have added in each UICollectionViewCell. Before diving into the problem, let me brief out all I've got so far.
Here's the basic rundown of how my UICollectionView in Scrolling Filmstrip style within UITableView works:
Create a normal UITableView with a custom UITableViewCell
Create a custom UIView that will be added to the cell's contentView
The custom UIView will contain a UICollectionView
The custom UIView will be the datasource and delegate for the
UICollectionView and manage the flow layout of the UICollectionView
Use a custom UICollectionViewCell to handle the collection view data
Use NSNotification to notify the master controller's UITableView when
a collection view cell has been selected and load the detail view.
So far, I've been able to add UIButton in each UICollectionViewCell and when I tap the UIButton its image will change to checked mark but the problem occurs when I scroll up/down. Once the cell with checked mark UIButton is off-screen and scrolled back on-screen again, UIButton image starts to get messed around. For example, when UIButton image in cell1 should be checked mark but it's not. Instead checked mark will be appeared in another cell.
Here below is my relevant code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
PostsCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"PostsCollectionViewCell" forIndexPath:indexPath];
NSDictionary *cellData = [self.collectionData objectAtIndex:[indexPath row]];
cell.postTextLabel.text = [cellData objectForKey:#"text"];
cell.locationNameLabel.text = [cellData objectForKey:#"locationName"];
// >>> Select Button <<<
UIButton *selectButton = [UIButton buttonWithType:UIButtonTypeCustom];
[selectButton setFrame:CGRectMake(110, 150, 20, 20)];
[selectButton setImage:[UIImage imageNamed:#"uncheckedDot.png"] forState:UIControlStateNormal];
[selectButton setImage:[UIImage imageNamed:#"checkedDot.png"] forState:UIControlStateSelected];
[cell.contentView addSubview:selectButton];
[selectButton addTarget:self
action:#selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
// >>> End Select Button <<<<
return cell;
}
// >>> Select Button Method <<<
-(void) buttonPressed:(UIButton *)sender
{
if([sender isSelected]){
//...
[sender setSelected:NO];
} else {
//...
[sender setSelected:YES];
}
}
Any help would be greatly appreciated! Thanks in advance.

Man, you should be aware that cells (both for UITableView and UICollectionView) are reused via the reuse deque under the hood of UIKit and are not obligated to keep their state consistent with regard to their position in a table view or a collection view. What you need to do is to make sure that the return value of dequeueReusableCellWithReuseIdentifier: is then properly initialized (or, in your case, re-initialized) in accordance with the state of your data model. Basically, you need to store the state of your "checkmarks" (or "checkboxes", what have you) in an array or any other data structure. Then, when you call dequeueReusableCellWithReuseIdentifier:, you should apply that stored state to the return value (the cell you're going to return for the collection view) you just got. Hope it's explained good enough. Good luck.

Maintain in your Data source which cell is to be selected by adding a key for each data source dict. For example:#"isSelected"
Then in cellForRowAtIndexPath:
if ([[cellData valueForKey:#"isSelected"] boolValue]) {
[selectButton setSelected:NO];
} else {
[selectButton setSelected:YES];
}
and in your -(void) buttonPressed:(UIButton *)sender Method:
CGPoint point = [self.collectionView convertPoint:CGPointZero fromView:sender];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
NSDictionary *cellData = [self.collectionData objectAtIndex:[indexPath row]];
if ([[cellData valueForKey:#"isSelected"] boolValue]) {
[cellData setValue:#"false" forKey:#"isSelected"];
} else {
[cellData setValue:#"true" forKey:#"isSelected"];
}
[self.framesCollectionView reloadItemsAtIndexPaths:#[indexPath]];
AND
In your cellForRowAtIndexPath set a tag for the UIButton. Then before adding a UIButton as subview to the cell's contentView check if a view with this tag already exists. In case the cell has been reused, just use the already existing button.

Related

Strange behavior when adding UIButton to UICollectionViewCell

I'm trying to add aUIButton to aUICollectionViewCell. The main purpose of the button is to let users to select their favourites.
When they click on theUIButton, the UIButton will display a glowing star image, showing that the item has been picked as favourite.
Click on the UIButton again, and the button return to display the original image with an empty star, meaning deselection.
The way I realize this is as below:
1 - In the storyboard, put an UIButton into theUICollectionViewCell, ctrl-drag the button to the cell's .h file to setup an action called
-(IBAction)favoriteBtnClick:(id)sender;
2 - In the cell's .m file, edit the action as follows:
-(IBAction)favoriteBtnClick:(id)sender {
if ([self.favoriteChecked isEqualToString:#"NO"]) {
UIImage *image = [UIImage imageNamed:#"FavoriteSelected.PNG"];
[sender setBackgroundImage:image forState:UIControlStateNormal];
self.favoriteChecked = #"YES";
} else if ([self.favoriteChecked isEqualToString:#"YES"]) {
UIImage *image = [UIImage imageNamed:#"FavoriteBlank.PNG"];
[sender setBackgroundImage:image forState:UIControlStateNormal];
}
}
However, when I try to run the UICollectionView, when I clicked the button on one cell, "strange" behaviors occurred:
1 - Click on one cell to turn one button to favorite glowing star, buttons on some other cells below also turn selected, after your scroll down the list.
2 - The switching only functions for one cycle. After you finish the first round of select and deselect, there is no longer reactions upon further clicking.
Can anybody help me understand the reasons for this and probably give me some hints on how to solve it?
Thank you!
Regards,
Paul
Add this code in your button action
CGPoint ButtonPoint = [sender convertPoint:CGPointZero toView:self.collectionView];
NSIndexPath *ButtonIndex = [self.collectionView indexPathForItemAtPoint:ButtonPoint];
buttonindexpath = ButtonIndex.row
[collectionView reloadData];
Now in your cellForItemAtIndexPath, check if indexPath.row == buttonindexpath, if yes give the code you gave in button action....
ie....
if (indexPath.row == buttonindexpath)
{
if ([self.favoriteChecked isEqualToString:#"NO"])
{
UIImage *image = [UIImage imageNamed:#"FavoriteSelected.PNG"];
[sender setBackgroundImage:image forState:UIControlStateNormal];
self.favoriteChecked = #"YES";
}
else if ([self.favoriteChecked isEqualToString:#"YES"])
{
UIImage *image = [UIImage imageNamed:#"FavoriteBlank.PNG"];
[sender setBackgroundImage:image forState:UIControlStateNormal];
}
}
Hope this works fine
This is happening because of cell reuse. since self.favoriteChecked is a property inside the custom collection cell class, and its value is updated only inside that class causes this issue.
Solution to this problem is, if you are using custom object model to populate collection cells from viewcontroller, then on favourite button click update the favourite status on that custom object and in cellForItemAtIndexPath method check the value of favourite status and update the favoriteChecked status of custom collectionviewcell class.
If you are not using custom object, you need to keep a list of favourite items separately either indexpaths of favourite item or unique identifier of fav item. Then in cellForItemAtIndexPath method check the value of favourite status and update the favoriteChecked status of custom collectionviewcell class.
This is no no a strange behaviour. You have load the cells from the model data objects. You have to track & keep the state change of button clicked data in an array & then load the cell data values.
Here is a sample code : which aligns with solution to your problem.
NSMutableArray *favoriteStatusArray = [[NSMutableArray alloc]init];
// assume favoriteStatusArray is initialized with all BOOL as NO.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
SomeCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
cell.favoriteButton.tag = indexPath.row;
[cell.favoriteButton addTarget:self action:#selector(peformFavoriteChange:) forControlEvents:UIControlEventTouchUpInside];
if([[favoriteStatusArray objectAtIndex:indexPath.row] boolValue] == NO)
UIImage *image = [UIImage imageNamed:#"FavoriteSelected.PNG"];
[cell.favoriteButton setBackgroundImage:image forState:UIControlStateNormal];
}else {
UIImage *image = [UIImage imageNamed:#"FavoriteBlank.PNG"];
[cell.favoriteButton setBackgroundImage:image forState:UIControlStateNormal];
}
return cell;
}
-(void)peformFavoriteChange:(UIButton *)sender
{
BOOL oldValue = [[favoriteStatusArray objectAtIndex:sender.tag] boolValue];
[favoriteStatusArray replaceObjectAtIndex:sender.tag withObject:[NSNumber numberWithBool:!oldValue]];
[myCollectionView reloadData];
}
Hope you got the idea of doing this.

Some rows never have UIButtons when UITableView is created

I have a UITableView that is never consistent. This table view has 2 sections.
I have created 2 UIButtons, they both are practically the same image, but one is grey and one is red.
Every row in the UITableView is supposed to have the grey button when the UITableView first loads, and then if a user taps a button then it switches to the red version of the button.
For some reason, about 80% of the time when I load the view controller with this UITableView on my iPhone, the first 2 rows in the first section will not have a button. What's weird is if I tap my finger where the button is supposed to be 2 things happen:
The functionality attached to the button works and NSLogs in the console in xcode.
The red button appears.
So it's like the grey button is there, it's just invisible. This doesn't work because if a user sees 2 rows without a button they are going to think something is broken or that they can't tap on that row.
Also, if I scroll to the very bottom of my UITableView, and then scroll back up again, the invisible grey buttons will magically appear like they were there the whole time.
Here are the first 3 method implementations that are involved in creating my UITableView:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2 ;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if(section == 0)
return [self.potentiaFriendsInParseFirstNamesArray count];
if(section == 1)
return [self.potentiaFriendsNotInParseFirstNamesArray count];
else return 0;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if(section == 0)
return #"Friends Using The App";
if(section == 1)
return #"Send to Contact";
else return #"nil";
}
And then here is my cellForRowAtIndexPath method implementation which does most of the work in creating the UITableView and which actually creates the UIButton settings for each row. The if else statement really has 2 sets of nearly identical code, the only difference is one is for section 1 of the table view and the other is for section 2:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"SettingsCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (indexPath.section == 0) {
NSString *firstNameForTableView = [self.potentiaFriendsInParseFirstNamesArray objectAtIndex:indexPath.row];
PFUser *user = [self.allUsersInParse objectAtIndex:indexPath.row];
UIImage *addUserButtonImage = [UIImage imageNamed:#"SliderThumb-Normal-g"];
UIImage *addUserButtonImageHighlighted = [UIImage imageNamed:#"SliderThumb-Normal"];
UIButton *addUserButton = [[UIButton alloc]init];
addUserButton.frame = CGRectMake(237, -10, 64, 64);
[addUserButton setImage:addUserButtonImage forState:UIControlStateNormal];
[addUserButton setImage:addUserButtonImageHighlighted forState:UIControlStateHighlighted];
[addUserButton setImage:addUserButtonImageHighlighted forState:UIControlStateSelected];
[addUserButton addTarget:self action:#selector(handleTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
addUserButton.tag = indexPath.row;
[cell.textLabel setText:firstNameForTableView];
[cell.detailTextLabel setText:user.username];
[cell.contentView addSubview:addUserButton];
} else {
NSString *firstNameForTableView2 = [self.potentiaFriendsNotInParseFirstNamesArray objectAtIndex:indexPath.row];
NSString *userNameForTableView2 = [self.potentiaFriendsPhoneNumberArray objectAtIndex:indexPath.row];
UIImage *addFriendButtonImage = [UIImage imageNamed:#"SliderThumb-Normal-G"];
UIImage *addFriendButtonImageHighlighted = [UIImage imageNamed:#"SliderThumb-Normal"];
UIButton *addFriendButton = [[UIButton alloc]init];
addFriendButton.frame = CGRectMake(237, -10, 64, 64);
[addFriendButton setImage:addFriendButtonImage forState:UIControlStateNormal];
[addFriendButton setImage:addFriendButtonImageHighlighted forState:UIControlStateHighlighted];
[addFriendButton setImage:addFriendButtonImageHighlighted forState:UIControlStateSelected];
[addFriendButton addTarget:self action:#selector(handleTouchUpInsideForNonUsers:) forControlEvents:UIControlEventTouchUpInside];
addFriendButton.tag = indexPath.row;
[cell.textLabel setText:firstNameForTableView2];
[cell.detailTextLabel setText:userNameForTableView2];
[cell.contentView addSubview:addFriendButton];
}
return cell;
}
I have not included the full method implementations that these 2 statements trigger because they only handle the functionality attached to each button, and most likely have nothing to do with this issue, but I will post them here anyways. These 2 statements can be found in the if else for cellForRowAtIndexPath and handle the touch events for the 2 sections of the table view:
[addFriendButton addTarget:self action:#selector(handleTouchUpInsideForNonUsers:) forControlEvents:UIControlEventTouchUpInside];
[addFriendButton addTarget:self action:#selector(handleTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
I see the pattern of trying to set up a custom UITableViewCell from within tableView:cellForRowAtIndexPath: quite a lot. The trouble is that I believe it to be the wrong place to do it. If you're really only using a built-in UITableViewCell, then it's the right place; for custom cells, it's not.
I'd recommend making a UITableViewCell subclass with a "setup" method that takes an object and uses that object to set up the cell's UI.
For instance, take section 0 from your UITableView. I'd do something like this:
#interface UserTableViewCell : UITableViewCell
// properties to IBOutlet connections for UI setup
- (void)setupWithUser:(PFUser *)user;
#end
#implementation UserTableViewCell
- (void)setupWithUser:(PFUser *)user {
// Set up your UI with properties from 'user'
}
- (IBAction)buttonPressed:(UIButton *)sender {
// Respond to the button press
}
This way, you can set up the cell's layout as well as images for the button in IB, rather than code. This also makes the code a lot easier to read (and re-read 6 months later!).
Also, unless the 2 cells (section 0 and section 1) are really interchangeable, I'd recommend two different subclasses of UITableViewCell to give you greater control, rather than trying to force one to act like the other or vice versa. Then, you simply assign different identifiers and recycle the appropriate type.

UIButton Image Changing In Multiple UITableViewCells

I am currently building an iOS app that uses a UITableView with custom UITableViewCells that include a button. What I want to accomplish is having the UIButton's image change on touch up inside. That part is working fine, the issue is when you scroll, suddenly every few row buttons located in the UITableViewCell have this new image. Below is some of my code. Any help would be appreciated.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *cellID = #"CellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellID];
//YEP BUT
yepBut = [[UIButton alloc] initWithFrame:CGRectMake(23, 16, 64, 32.5)];
[yepBut addTarget:self action:#selector(yepPost:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:yepBut];
}
[yepBut setImage:[UIImage imageNamed:#"yepBtn"] forState:UIControlStateNormal];
return cell;
}
-(void) yepPost:(id) sender{
//UIButton *clicked = (UIButton *) sender;
[sender setImage:[UIImage imageNamed:#"yepBtnACT"] forState:UIControlStateNormal];
}
Thanks!
You need to have a property that you set when the button is touched that you check in cellForRowAtIndexPath. So, in the button's method, have a property, say lastTouched (#property (strong,nonatomic) NSIndexPath *lastTouched, or NSInteger if you don't have sections), that you set to the indexPath (or indexPath.row) of the cell in which the touched button resided (gotten from the cell which you get by searching up through the superviews of the button until you find the cell, or by using tags on your buttons equal to the indexPath.row). After that, you have to reloadData or reloadRowsAtIndexPaths to make the change happen. In cellForRowAtIndexPath, you would set your images like this:
if ([self.lastTouched isEqual:indexPath]) {
[yepBut setImage:[UIImage imageNamed:#"yepBtnACT"] forState:UIControlStateNormal];
else{
[yepBut setImage:[UIImage imageNamed:#"yepBtn"] forState:UIControlStateNormal];
}
When you first initialize lastTouched, you want to set it to an indexPath that doesn't occur in your table, so nothing will have the yepButAct image until you touch one.
Subclassing UITableViewCell fixed my issue.

Losing UIButton within a custom cell when reloading table

I've custom UITableViewCells where I placed buttons dynamically in code. I have programmatically set actions for such buttons, and when the action is triggered, then I need to reload the table, among other things. Once reload is called, the sender button disappears, and I don't know why... Could somebody give me a hint for the possible reason? If button is not tapped, and I scroll up and down through the table, it remains there.
Thanks!
EDIT
This is a code snippet from cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *listData =[self.tableViewEntries objectForKey:[self.sortedKeys objectAtIndex:[indexPath section]]];
UITableViewCell *cell;
if (indexPath.section == 0) {
switch (indexPath.row) {
case 0: {
cell = [self.tableView dequeueReusableCellWithIdentifier:#"firstCell"];
((CustomCell *)cell).cellTextField.tag = firstCellTag;
cell.tag = firstCellTag;
break;
}
case 1: {
cell = [self.tableView dequeueReusableCellWithIdentifier:#"secondCell"];
((CustomCell *)cell).cellTextField.tag = secondCellTag;
cell.tag = secondCellTag;
break;
}
}
return cell;
}
The CustomCell I've defined is loaded from a nib. Those cells have a UITextField for user input and a UIView where I set or remove the buttons dynamically in code according to the user input within the text field. The buttons are created in code, and their actions are set in code as well:
- (void)setButtonForCell:(UITableViewCell *)cell withResult:(BOOL)isValid
{
UIImage* validationButtonNormalImage = nil;
UIImage* validationButtonHighlightImage = nil;
UIButton *validationButton = [UIButton buttonWithType:UIButtonTypeCustom];
[validationButton setFrame:CGRectMake(0, 0, ((CustomCell *)cell).cellValidationView.frame.size.width, ((CustomCell *)cell).cellValidationView.frame.size.height)];
if (isValid) {
validationButtonNormalImage =[[UIImage imageNamed:#"success.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0];
validationButtonHighlightImage =[[UIImage imageNamed:#"success.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0];
[validationButton removeTarget:self
action:#selector(validationButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
}
else {
validationButtonNormalImage =[[UIImage imageNamed:#"error.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0];
validationButtonHighlightImage =[[UIImage imageNamed:#"error.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0];
[validationButton setTag:cell.tag];
[validationButton removeTarget:self
action:#selector(validationButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
[validationButton addTarget:self
action:#selector(validationButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
}
[validationButton setBackgroundImage:validationButtonNormalImage forState:UIControlStateNormal];
[validationButton setBackgroundImage:validationButtonHighlightImage forState:UIControlStateHighlighted];
[((CustomCell *)cell).cellValidationView.subviews makeObjectsPerformSelector: #selector(removeFromSuperview)];
[((CustomCell *)cell).cellValidationView addSubview:validationButton];
}
Then, in validationButtonTapped:, I do some tasks and call to [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:NO];, and there is when I loose the button from the view. However, if I don`t tap the button and I scroll down and up again to get 'cellForRowAtIndexPath:' called again, the button remains here. It is like I only loose it when calling for a reload.
Any help?
Cells are usually reused if you are following the usual cellForRowAtIndexPath: pattern. Do you remove all subviews in prepareForReuse for each cell? If you didn't then you would see unexpected buttons on cells as you scroll up and down. You probably need to add the buttons as required in each cell's configuration after you get the cell for reuse (or in newer code, dequeue a cell) in cellForRowAtIndexPath:.

UICollection view not redrawing gradient buttons

I have a UICollectionView, which is populated with cells, and each cell has a gradient button (MKgradientbutton https://github.com/mikekatz/iOS-UI-Utils/tree/master/Classes/Button%20Widgets). The view is 'changed' by pressing one of 5 other buttons, which in turn loads the data for the buttons into an array which provides the datasource for the uicollectionview. One of the paramaters is the gradient button colour. However on reloading the datasource and performing a [self.collectionView reloadData] the buttons title and the other paramaters will change but the colour will quite often change to completely the wrong one. Repeatedly reloading the data will solve this problem - however, even more strangely, one press of the button seems to correct the colour of the button! I have NSLogged the datasource array on the line before the reloadData takes place and all of the colours are correct. I have pasted some code below - please bear with me as the app is still in the very early stages of development!
I really can't see any issues with the code - and I believe that the UICollectionView needs 'redrawn' but I assumed [self.CollectionView reloadData] would solve that!
Pulling data from sqlite db into and object then adding to array:
Bird *bird = [[Bird alloc] init];
bird.birdName = [results stringForColumn:#"name"];
bird.buttonColour = [results stringForColumn:#"btncolour"];
[birds addObject:bird];
The array 'birds' being used to populate UICollectionView:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView2 cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = [collectionView2 dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
Bird *bird = [birds objectAtIndex:(indexPath.row)];
//Check button colour against index path of item
NSLog(#"****INDEX PATH %i",indexPath.section*2 + indexPath.row);
NSLog(#"****colour %#",bird.buttonColour);
[cell.button setTitle:bird.birdName forState:UIControlStateNormal];
[cell.button setTag:[birds indexOfObject:bird]];
if ([bird.buttonColour isEqualToString:#"Green"]) {
[cell.button setButtonColor:[UIColor greenColor]];
}
if ([bird.buttonColour isEqualToString:#"Orange"]) {
[cell.button setButtonColor:[UIColor orangeColor]];
}
if ([bird.buttonColour isEqualToString:#"Red"]) {
[cell.button setButtonColor:[UIColor redColor]];
}
if ([bird.buttonColour isEqualToString:#"Blue"]) {
[cell.button setButtonColor:[UIColor blueColor]];
}
cell.button.titleLabel.numberOfLines = 0;
cell.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.button.titleLabel.textAlignment = NSTextAlignmentCenter;
return cell; }
collectionViewCell.h (the gradient button is the only item in the cell):
#import <UIKit/UIKit.h>
#import "MKGradientButton.h"
#interface CollectionViewCell : UICollectionViewCell
#property (nonatomic) IBOutlet MKGradientButton* button;
#end
You may have to tell the button to redraw itself via [cell.button setNeedsDisplay].

Resources