I have a tableview with cells (not custom), and each cell has a button (among other things).
Depending on the cell, the button that is created is different. I have 3 different types of cell/button, Rate/View/Edit (I'm not sure that's at all relevant)
This button is only used for this:
[button addTarget:self action:#selector(rateEvent:) forControlEvents:UIControlEventTouchUpInside];
The selector is different for each button (rateEvent, viewEvent, editEvent).
Here is an example, they're all 3 fairly similar and start the segue sequence:
- (IBAction)viewEvent:(UIButton*)sender
{
[self performSegueWithIdentifier:#"fromHomeToDetails" sender:self];
}
My didSelectedRowAtIndexPath saves the details of a meeting in the selectedMeeting object.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
selectedMeeting = [_nextMeetingsArray objectAtIndex:indexPath.row];
}
The segue sends selectedMeeting to the next controller.
My issue : The meeting that is sent depends on the selected cell. That means the user has to select a cell and THEN tap the button to get the correct details on the next page. What can i do to get the correct indexpath of a tapped button inside a cell?
I tried to add parameters to the viewEvent method, to make it look like -(void)viewEvent:(int)index:(UIButton*)sender; but it doesn't work in the #selector(viewEvent:).
The solutions i can imagine but not create are :
- Manage to get the indexpath of the cell of a tapped button and send it to my viewEvent method, somehow.
- Create a customCell and/or use an accessory, so i've read here and there.
- Force row selection when i tap the button, but that also requires to know the index of said row.
Both seem "too complicated" for something that (should?) be fairly simple. I'm pretty sure any experienced programmer will have an obvious easy answer that I haven't learned yet, and i'd rather ask for it than implement something heavier than it should be.
Also, if you have any comment/criticism on how i've done that, i'm all ears :)
Thanks for your time guys, as always, i'm very grateful.
EDIT :
cellForRow method :
Note : i removed everything that has to do with labeling. Because i have different buttons there are multiple if{} statements but they are of no influence in my opinion. Also, the cell is based on a storyboard cell with a tag of 228 ; after creation a new tag is set so it shouldn't influence. Also, i have a cap of 50 items in tableview (set from webservice) so there is no way the indexpath reaches >50.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *nextCell = [self.tbvNextMeetings dequeueReusableCellWithIdentifier:#"tbNextCell" forIndexPath:indexPath];
UIButton *bt= (UIButton*) [nextCell viewWithTag:228];
[bt setTitle:#"Edit" forState:UIControlStateNormal];
[bt addTarget:self action:#selector(editEvent:) forControlEvents:UIControlEventTouchUpInside];
[bt setTag:indexPath.row];
return nextCell;
Add tag to button as indexpath.row, in cellForRowAtIndexPath() method. Then in selector method get sender tag. And pass in
NSIndexPath *path = [NSIndexPath indexPathForRow:sender.tag inSection:yoursection];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
Edit : Setting the cell with a tag of 228 at first and then switching it to the value of indexpath made its creation fail.
Related
I'm using a button inside a tableView in which I get the indexPath.row when is pressed. But it only works fine when the cells can be displayed in the screen without scroll.
Once the tableView can be scrolleable and I scrolls throught the tableview, the indexPath.row returned is a wrong value, I noticed that initially setting 20 objects, for example Check is just printed 9 times no 20.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
//This is printed just 9 times (the the number of cells that are initially displayed in the screen with no scroll), when scrolling the other ones are printed
NSLog(#"Check: %li", (long)indexPath.row);
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
NSLog(#"Pressed: %li", (long)sender.tag);
}
Constants.h
#define ROW_BUTTON_ACTION 9
What is the correct way to get the indexPath.row inside rowButtonClicked or setting a tag when I have a lot of of cells in my tableView?
My solution to this kind of problem is not to use a tag in this way at all. It's a complete misuse of tags (in my opinion), and is likely to cause trouble down the road (as you've discovered), because cells are reused.
Typically, the problem being solved is this: A piece of interface in a cell is interacted with by the user (e.g. a button is tapped), and now we want to know what row that cell currently corresponds to so that we can respond with respect to the corresponding data model.
The way I solve this in my apps is, when the button is tapped or whatever and I receive a control event or delegate event from it, to walk up the view hierarchy from that piece of the interface (the button or whatever) until I come to the cell, and then call the table view's indexPath(for:), which takes a cell and returns the corresponding index path. The control event or delegate event always includes the interface object as a parameter, so it is easy to get from that to the cell and from there to the row.
Thus, for example:
UIView* v = // sender, the interface object
do {
v = v.superview;
} while (![v isKindOfClass: [UITableViewCell class]]);
UITableViewCell* cell = (UITableViewCell*)v;
NSIndexPath* ip = [self.tableView indexPathForCell:cell];
// and now we know the row (ip.row)
[NOTE A possible alternative would be to use a custom cell subclass in which you have a special property where you store the row in cellForRowAt. But this seems to me completely unnecessary, seeing as indexPath(for:) gives you exactly that same information! On the other hand, there is no indexPath(for:) for a header/footer, so in that case I do use a custom subclass that stores the section number, as in this example (see the implementation of viewForHeaderInSection).]
I agree with #matt that this is not a good use of tags, but disagree with him slightly about the solution. Instead of walking up the button's superviews until you find a cell, I prefer to get the button's origin, convert it to table view coordinates, and then ask the table view for the indexPath of the cell that contains those coordinates.
I wish Apple would add a function indexPathForView(_:) to UITableView. It's a common need, and easy to implement. To that end, here is a simple extension to UITableView that lets you ask a table view for the indexPath of any view that lies inside one of the tableView's cells.
Below is the key code for the extension, in both Objective-C and Swift. There is a working project on GitHub called TableViewExtension-Obj-C that illustrates the uses of the table view extension below.
EDIT
In Objective-C:
Header file UITableView_indexPathForView.h:
#import <UIKit/UIKit.h>
#interface UIView (indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view;
#end
UITableView_indexPathForView.m file:
#import "UITableView_indexPathForView.h"
#implementation UITableView (UITableView_indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view {
CGPoint origin = view.bounds.origin;
CGPoint viewOrigin = [self convertPoint: origin fromView: view];
return [self indexPathForRowAtPoint: viewOrigin];
}
And the IBAction on the button:
- (void) buttonTapped: (UIButton *) sender {
NSIndexPath *indexPath = [self.tableView indexPathForView: sender];
NSLog(#"Button tapped at indexpPath [%ld-%ld]",
(long)indexPath.section,
(long)indexPath.row);
}
In Swift:
import UIKit
public extension UITableView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
I added this as a file "UITableView+indexPathForView" to a test project to make sure I got everything correct. Then in the IBAction for a button that is inside a cell:
func buttonTapped(_ button: UIButton) {
let indexPath = self.tableView.indexPathForView(button)
print("Button tapped at indexPath \(indexPath)")
}
I made the extension work on any UIView, not just buttons, so that it's more general-purpose.
The nice thing about this extension is that you can drop it into any project and it adds the new indexPathForView(_:) function to all your table views without having do change your other code at all.
You are running into the issue of cell-reuse.
When you create a button for the view you set a tag to it, but then you override this tag to set the row number to it.
When the cell get's reused, because the row number is longer ROW_BUTTON_ACTION, you don't reset the tag to the correct row number and things go wrong.
Using a tag to get information out of a view is almost always a bad idea and is quite brittle, as you can see here.
As Matt has already said, walking the hierarchy is a better idea.
Also, your method doesn't need to be written in this way. If you create your own custom cell, then the code you use to create and add buttons and tags isn't needed, you can do it in a xib, a storyboard, or even in code in the class. Furthermore, if you use the dequeue method that takes the index path, you will always get either a recycled cell, or a newly created cell, so there is no need to check that the cell returned is not nil.
I used this code in cellForRowAtIndexPath method. When I click on button or scroll table this button is shown two times. Why is this button shown 2 times please help me?
UIButton *trashbtn=[[UIButton alloc]initWithFrame:CGRectMake(cell.frame.size.width-20, cell.frame.size.height-30, 20, 20)];
[trashbtn setImage:[UIImage imageNamed:#"editor_trash"] forState:UIControlStateNormal];
[trashbtn addTarget:self action:#selector(DeleteMyAssociate:) forControlEvents:UIControlEventTouchUpInside];
[trashbtn setTag:indexPath.row];
[cell addSubview:trashbtn];
You should firstly add a UITableViewCell within your table in IB. Then give an identifier e.g. "MyCellIdentifier" to that cell. Still on IB, add your outlets, to that cell e.g. the button, the textFields... You can initially set the button to be invisible. Then in the method cellForRowAtIndexPath, you do:
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCellIdentifier"];
if(cell){
myButton.hidden = NO; //
myTextField.text = #"BlaBlaBla";
}
}
Hey I can give you a suggestion that use auto layout and storyboard to add button rather than adding it programmatically. That is more better and cleaner approach.
The problem you are facing is that cells are reused in tableviews, So the first time you create a button and add it to the cell, it appears once. But if it is dequeued, it already has the button, so when you add another one you end up with multiple buttons.
There are two ways to correct this.
Firstly, remove the button in the cell's prepareForReuse method, which is called just after the cell is reused.
Secondly, avoid creating custom views in the cellForTableView... method. Use a custom cell that already has the button. Now, you may ask, how do I hook up the action for the button if I do it this way? You can either provide a delegate method for your cell that calls back to your view controller, or you can pass your cell a block to perform when the button is clicked.
as far as i can get u already have a custom cell.. add a button to existing cell and create an IbOutlet for the same in customCell.h file
then in cellForRowAtIndexPath method access the button using its IBOutlet
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
customCell *cell =(customCell*) [tableView dequeueReusableCellWithIdentifier:#"customCell" forIndexPath:indexPath];
[cell.btn setTitle:#"blabla" forState:UIControlStateNormal];
[cell.btn setTag:indexPath.row];
[cell.btn addTarget:self action:#selector(method:) forControlEvents:UIControlEventTouchUpInside];
if(condition)
{
cell.btn.hidden=YES;
}
else
{
cell.btn.hidden = NO;
}
return cell;
}
Do not forget to register for the customCell class in your ViewController class
[tableName registerNib:[UINib nibWithNibName:#"customCell" bundle:nil] forCellReuseIdentifier:#"customCell"];
you can hide or unhide the button as per your requirements as well as add different actions to the button in each class.
Happy Coding..!!
Do vote it if my code was helpful for you.. ;)
Hello I have recently met up with a problem on UITableViewCell
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"ContentCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
CloseButton *cButton = (CloseButton *)[cell viewWithTag:20];
[cButton addTarget:self action:#selector(deleteDetector:) forControlEvents:UIControlEventTouchUpInside];
...
return cell;
}
Later, on my delete detector:
-(void)deleteDetector:(id)sender {
CloseButton *cButton = (CloseButton *)sender;
[cButton setHidden:YES];
}
When I start scrolling down to like 1000 cells, the buttons start to appear and some of them starts to disappear.
Ok, so if I understand your question correctly, I assume what's going on is:
You are pressing the button on a cell, which makes the button hidden. Then, you scroll further down, and another cell appears with the button already hidden (even though you haven't pressed the button for that row yet).
This is because your cells are actually being reused, meaning that when one of the cells that has already has the button hidden gets reused, that button will still be hidden (as it is actually the same cell). The 'quick fix' to prove this is to unhide the button in your tableView:cellForRowAtIndexPath: method, like so:
[cButton setHidden:NO];
Do this somewhere after this, obviously:
CloseButton *cButton = (CloseButton *)[cell viewWithTag:20];
This should prevent cells appearing with the button hidden when they shouldn't. However, it will also mean that if you press the button on a cell, and it then goes off screen and comes back on, it will also then show the button again, when you probably don't want it to. You'll have to keep a track of which rows you have pressed the button on in your model somewhere if you don't want that to happen.
When it comes to write a table to present and organise the application settings area I feel always a bit "confused". I think that the better way would be to go with Static table, but I find extremely hard to customise it (especially because I can't customise grouped view cell).
So I often end up with a common table with a datasource. My doubts is related with cells that should directly presents the options information.
Let's say that I need 2 kind of cells.
A cell that shows a switch
A cell that shows the current value for the option
My doubts essentially are:
Which is the best way to setup the cells? Should I create a single subclass and just show or hide the label or the switch during the cellForItemAtIndex process?
Which is the best way to interact with the cells? When user changes the value of a switch... how can I set a reference to that switch and the right cell containing it?
What about the datasource? at the moment I implement a switch and depending on the required index I set up the cell
Can you kindly share some thoughts/examples of code about how you implement you settings?
This answer is based on the things i've done in my own app(s).
I wouldn't subclass UITableViewCell unless I wanted to do something "really" custom. I would just use the standard UITableViewCell, create a UISwitch control, or a label, depending on what you want to show, and add it as the cell's accessory. For instance:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * cellIdentifier = #"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
cell = [[TSOptionCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
if (indexPath.row == 0) /* or whatever index you want to set the switch or label at */
{
UISwitch * switch = [[UISwitch alloc] initWithFrame:frame];
[switch addTarget:self action:#selector(toggleSwitch:) forControlEvents:(UIControlEventValueChanged | UIControlEventTouchDragInside)];
/* create your switch and add it's target and action (method that's called when it is toggled) */
// Set the value of the switch
switch = [[NSUserDefaults standardUserDefaults] boolForKey:#"key"];
[cell setAccessoryView:switch];
}
}
The method that you added as the "action" for the switch above gets called when the user interacts with the switch. For instance, lower down in your code somewhere, you'd have this:
-(void)toggleSwitch:(id)sender
{
/* handle user interaction somehow */
}
This is what i currently do with my app in the app store. I use a switch statement to check the indexPath.row against the index of where I want the switch to be displayed, so I think this is appropriate.
Another way is to create two prototype cells in the storyboard, with different identifiers, and the in
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
you should use each cell at the index you need.
About the second issue in cellForRowAtIndexPath you should save the current state of the switch in an NSMutableArray.
you set a tag for each cell and in
- (void)toggleSwitch:(id)sender {
NSInteger currentIndex;
currentIndex = [sender superview].tag;
[yourMutableArray replaceObjectAtIndex:currentIndex withObject:![NSNumber numberWithBool:[yourMutableArray objectAtIndex:currentIndex]BoolValue];
}
of course, in your cellForRow you set the value of the array like:
[yourMutableArray insertObject:[NSNumber numberWithBool:[mySwitch isOn] atIndex:indexPath.row];
Hope this is what you were searching
I have simply dragged a custom style button to a cell in storyboards. The problem is that when I press the button, it clicks the cell and not the button.
What could cause this?
Do I need to increase the click area? If so how?
Do I need to bring the button to the front? If so how?
Thanks!
My cell for row at index path looks like:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
MainUserViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
[cell initWithStyle:nil reuseIdentifier:nil];
[cell.descrlptionLabel sizeToFit];
[cell.userNameButton addTarget:self action:#selector(userNameClicked:) forControlEvents:UIControlEventTouchDown];
return cell;
}
No need for the initWithStyle line in your method. You should eliminate it.
[cell initWithStyle:nil reuseIdentifier:nil];
The cell is dequeued and should already be ready and initialized. You are working with storyboard, obviously, so the dequeue method is guaranteed to return a valid cell, auto-creating it if necessary.
This is most likely messing things up.
Some more things to check: Make sure the button is topmost in your story board file (i.e. the bottom item in the list on the left). Your resizing of the label could cover it otherwise. Also, make sure you did not accidentally set userInteractionEnabled to NO.