UIButton with target:self not being sent to self? - ios

I'm creating a new UIViewController (call it MyViewController), and adding the view (MyView) as a subview of a TableViewCell. Within MyView , there's a button. That button is created programmatically during MyViewController's init, as such:
-(id)initWithFrame:(CGRect)frame {
self = [super init];
if(self) {
[self.view setFrame:frame];
_yesButton = [[UIButton alloc] initWithFrame:CGRectMake(frame.size.width-150, 40, 140, 30)];
[_yesButton setTitle:#"Yeah!" forState:UIControlStateNormal];
[_yesButton addTarget:self action:#selector(didClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_yesButton];
}
return self;
}
Now, seems straightforward. It displays properly, everything looks great in the simulator.
But when I click on the "_yesButton" within MyView, I get a crash with this error:
-[_UITableViewCellSeparatorView didClick]: unrecognized selector sent to instance 0x7b7f7ec0
What? When did "_UITableViewCellSeparatorView" come into the equation? I specifically told the _yesButton to set the Target to "self", so the selector should be sent to MyViewController, right? I could even imagine it getting tripped up and sending it to the UITableViewCell, since MyView is embedded within a TableViewCell, but why a SeperatorView?
Can anyone tell me how to get my _yesButton to send the call back to the MyViewController that it's being created within? And for bonus points, can you explain how "_UITableViewCellSeparatorView" became a thing in this conversation at all?
Edit: Here's how I'm building the cell in the TableView, and adding MyView to it. Note that I'm deliberately not using dequeuing for this row, although that might change if it's the source of the problem.
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"MyViewCell"];
MyViewController *myViewController = [[MyViewController alloc] initWithFrame:cell.contentView.frame];
[cell.contentView addSubview:myViewController.view];
return cell;
And the didClick method is currently empty, (it never even gets there, so I haven't gotten that far in writing it), but it's currently defined within MyViewController simply as:
-(void)didClick {
}

Solution #1
Actually this is because you MyViewController is not retain by ARC. The dealloc method id being called. Add ans instance of your controller in your UITableViewController will fix the issue and make ARC retains your controller.
Solution #2
Try something like this.
Create a custom UITableViewCell :
MyCustomCell.h :
#import <UIKit/UIKit.h>
#interface MyCustomCell : UITableViewCell
#property (strong, nonatomic) UIButton *yesButton;
-(id)initWithFrame:(CGRect)frame;
#end
MyCustomCell.m :
#import "MyCustomCell.h"
#implementation MyCustomCell
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(id)initWithFrame:(CGRect)frame {
self = [super init];
if(self) {
[self.view setFrame:frame];
_yesButton = [[UIButton alloc] initWithFrame:CGRectMake(frame)];
[_yesButton setTitle:#"Yeah!" forState:UIControlStateNormal];
[self.contentView addSubview:_yesButton];
}
return self;
}
#end
In your UITableViewControllerin the viewDidLoad function :
[self.tableView registerClass:[MyCustomCell class] forCellReuseIdentifier:CellIdentifier];
Then in your tableView:cellForRowAtIndexPath:
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifierr forIndexPath:indexPath];
if (cell == nil) {
cell = [[MyCustomCell alloc] initWithFrame:frame];
[cell.yesButton addTarget:self action:#selector(didClick) forControlEvents:UIControlEventTouchUpInside];
}
And now implement the following in your UITableViewController:
-(void)didClick {
// The following identify the in which cell the action has been triggered
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
if (indexPath != nil)
{
// Do whatever you want for the given cell
}
}

I think that you're adopting a wrong approach.
The view cells are not living in memory like views. We may say that once the drawing done, it does not exists anymore.
For all cells operations, you should set you TableViewController as target, and handle the click method in this controller.
Only this controller has a global knowledge of the data in the cells and the cells formats.
You can set the tag of the button to the row index to know what row has been clicked.
your didClick selector is also not correct. It should have this form
-(void)didClick:(id)sender
{
NSInteger clickedRow = ((UIButton*)sender).tag
}
And so be connected like this:
[_yesButton addTarget:<YourTableController> action:#selector(didClick:) forControlEvents:UIControlEventTouchUpInside];

The problem is that the instance you thing to be kind of MyViewController, it is no. It is instead UITableViewCellSeparatorView.
The biggest error, is that you are trying to init a view controller by a method which is not supported from the class UIViewController, but is instead part of UIView:
- (id)initWithFrame:(CGRect)frame
I suggest you to review it before going ahead.

Related

how to reload the Table view data when tap on UILabel(UITapGestureRecognizer)

I am new to objective c, and i am very interested in learn new technologies.
I have a UITapGestureRecognizer on a label. I am using 2 label ,when i tap on label1 load some data Tableview and when I tap on label 2 i want to change the Tableview.
I didn't find any solution for my requirement. so anyone help me .
suggest me guy's how to change the TableView data when tap on UILabel.
You can do this like following way:
Way 1:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
..
..
..
// Here is your stuff for cell
UILabel *lbl1 = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 40)];
[lbl1 setText:#"Label"];
[lbl1 setUserInteractionEnabled:YES];
[cell.contentView addSubview:lbl1];
UITapGestureRecognizer *tap=[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapAction:)];
tap.tag = [NSIndexPath row];
[tap setNumberOfTapsRequired:1];
[lbl addGestureRecognizer:tap];
...
}
- (void)tapAction:(id)sender {
switch(((UITapGestureRecognizer *)sender).view.tag) {
case 0:
// code for first TableView
break;
case 1:
// code for second TableView
break;
....
}
}
Way 2:
in cellForRowAtIndexPath: method:
UITapGestureRecognizer *tap=[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapAction:)];
tap.tag = [NSIndexPath row];
...
Handler method:
- (void)tapAction:(id)sender
{
switch(((UITapGestureRecognizer *)sender).tag)
{
case 0:
NSLog(#"Tap action 1");
break;
case 1:
NSLog(#"tap action 2");
break;
..... }
I assume you have setup the datasource/delegate methods for the tableview? Then change necessary things within these methods when the label is tapped and simply call
[self.tableView reloadData]
If you don't understand anything about what I'm saying then I advise you to first read some manuals about tableviews before starting asking questions on SO :)
Welcome to Stack Overflow!
This answer should help clear things up for you nicely and give you a stronger understanding of how to use table view's and buttons to do what you need.
FIRST
To Alladinian's comment, I highly recommend switching over to the UIButton object instead of using UILabel objects. This way your elements can still display your text (as you were with labels) but you are able to make use of the built-in target capabilities of the button class.
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
SECOND
Regarding your question, again, simply make use of an already built-in method that is designed for this very situation:
- (void)reloadData;
This method is available by using / setting UITableViewDelegate and the UITableViewDataSource. Like such: here is an example of a generic UIViewController class that conforms (can use properly) the convenient UITableView delegates.
Header File (SomeClassController.h)
// SomeClassController.h
#import <UIKit/UIKit.h>
#interface SomeClassController : UIViewController <UITableViewDelegate, UITableViewDataSource>
// do anything here. properties, methods, etc :)
#property (nonatomic, ...) UITableView *tableView;
#end
Implementation File (SomeClassController.m)
// ...
- (void)viewDidLoad {
self.tableView.delegate = self;
self.tableView.dataSource = self;
}
Now by using UIButton objects you can set a method that will handle the user touching the screen by performing a method. Like here:
- (void)viewDidLoad {
...
[someButton addTarget:self action:#selector(didTouchButton:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)didTouchButton:(__kindof UIButton *)button {
// Here you received the users touch event on the button, do what you like!
// As your question asked how to reload table data, simply add this line
// after following my other steps.
[self.tableView reloadData]; // Here the magic is done!
// This method will automatically reload the table view to display the
// most recently updated data you have given it.
}
Make sure you change your data before reloading, otherwise it may not change both visually or content-wise.
Happy coding!

Deallocated ViewController inside custom UITableViewCell

The structure of my code is this:
UITableViewController (with one or more)-> Custom UITableviewCell (add the view of)-> UIViewController
Now, to notify an action on the UIViewController to the UITableViewController I have a protocol that follow the inverse flow explained before, but, when I do some action on UIViewController, app crashes because I'm trying to access to a deallocated instance...
I avoid the crash on IBAction on UIViewController in a dirty way: setting a property in the UIViewController as self
How can I solved this leak? This is my code:
UITableViewController:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
GameTableViewCell *cell;
cell = [tableView dequeueReusableCellWithIdentifier: CellId];
[cell configureWithGame: currentGame];
cell.delegate = self;
return cell;
}
Custom TableViewCell:
-(void)configureWithGame:(Game *)game
{
outcomeController = [[OutcomeViewController alloc] initWithGame:game];
outcomeController.delegate = self;
activeGame = game;
//Adapting outcomeView
CGRect frame = outcomeController.view.frame;
frame.size = self.outcomeView.frame.size;
outcomeController.view.frame = frame;
[[self.outcomeView subviews] makeObjectsPerformSelector:#selector(removeFromSuperview)];
[self.outcomeView addSubview:outcomeController.view];
}
The OutcomeViewController has a property #property (nonatomic, strong) id forceRetain; and it sets in -(void)viewDidLoad in this way:
self.forceRetain = self;
This causes some leaks and I want to solve this issue.
try setting outcomeController property to strongin your cell code.
Moreover, with the code you posted, the OutcomeViewController will be allocated every time you scroll the UITableView. Is this the behavior you want?

UIView memory address changing randomly

I have a custom tableViewCell which contains a custom UIView. Within my cellForRowAtIndexPath I alloc and init my custom UIView and check the address of the UIView object. My custom UIView class also has a method called createArray that I call from cellForRowAtIndexPath that gives me an array with CGPoints that I use to create a path. I also check my UIView address at the end of my createArray method and it is consistent with what it was when I previously checked after I originally initialized the UIView. The createArray method works fine and gives me a valid array. However,after the method is done executing, layoutSubviews gets called immediately. What I noticed when I reach layoutSubviews is that my UIView address is changing and the array I created to create my path goes to nil. Anyone know whats happening?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* identifier = #"audioTableCell";
OSAudioTableCell *cell = [self.audioTable dequeueReusableCellWithIdentifier:identifier];
if(cell == nil)
{
cell = [[OSAudioTableCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: identifier];
}
self.waveView = [[OSWaveView alloc] init];
NSLog(#"self.view address %#", self.waveView);
[self.waveView createArray:transferredAsset andSetSize:cell.waveView.bounds.size];
}
-(void) layoutSubviews
{
if (!self.blueWave) {
self.blueWave = [self createShapeLayer];
self.blueWave.strokeColor = [UIColor blueColor].CGColor;
self.redWave = [self createShapeLayer];
self.redWave.strokeColor = [UIColor redColor].CGColor;
[self.layer addSublayer:self.blueWave];
[self.layer insertSublayer:self.redWave above:self.blueWave];
[self render];
}
}
Is layoutSubviews reinitializing my view or is there some background method that gets executed that is causing everything to go to nil? Also, I was thinking this might have to do with my UIView being an IBOutlet in storyboard and getting manually allocated/initialized.

Can I have a UIButton's action method take a second parameter?

I want to place a button in each cell of a table. I want the target to be a class that I'm using to handle all of the API calls. I want the button's method to be in the friendController class, not in the UITableView's View Controller.
[cell.deleteButton addTarget:friendController
action:#selector(deleteFriend: forID:cell.idNumber)
forControlEvents:UIControlEventTouchUpInside];
Where friendController is the class with the API calls, and cell.idNumber is an instance variable of each cell containing the ID number of the user associated with that cell.
I know (well, I presume) that I could set the cell itself as the target and then have a method which looks like:
-(IBAction)deleteFriend:(id)sender {
[friendController deleteFriend:self.idNumber];
}
Is that correct? It doesn't seem like an elegant way of doing this. Is there a way to do it the way I want to, or is there a better way to do it?
EDIT: I emboldened the last crucial part of my question. I want the button's action to be a method in another class (friendController). friendController is a class I created to house all of the API calls and business logic. If I set the target to friendController, must the action be a method in that class?
You can do this two ways.
Subclassing UIButton
The first would be to subclass UIButton and create a property for the idNumber. So you would first set the property like so:
cell.deleteButton.idNumber = /* set id here */
[cell.deleteButton addTarget:friendController
action:#selector(deleteFriend:)
forControlEvents:UIControlEventTouchUpInside];
and then access it inside your deleteFriend: method:
- (IBAction)deleteFriend:(id)sender {
YourButtonClass *button = (YourButtonClass *)sender;
[friendController deleteFriend:button.idNumber];
}
Retrieve the idNumber directly from the cell
Described in this post you can grab the index path of the button's cell and then access the idNumber from there.
Use the tag property of the button.
cell.deleteButton.tag = cell.idNumber;
[cell.deleteButton addTarget:friendController action:#selector(deleteFriend:) forControlEvents:UIControlEventTouchUpInside];
In the method, you can use the tag to retrieve the cell idNumber
-(IBAction)deleteFriend:(id)sender {
int idNumber = ((UIButton *) sender).tag;
[friendController deleteFriend: idNumber];
}
With 2 parameters indicating the control that sends the message and the event that triggered the message:
action:#selector(buttonAction:Second:)
- (void)viewDidLoad
{
[super viewDidLoad];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[btn addTarget:self action:#selector(buttonAction:Second:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)buttonAction:(NSString *)firstCharacter
Second:(NSNumber *)second
{
}
Since the button is inside a UITableViewCell, you might consider subclass a UITableViewCell with your property idNumber(I don't like reuse Tag because it can only be an integer) and a delegate:
#protocol MyTableViewCellDelegate;
#interface MyTableViewCell : UITableViewCell
#property (nonatomic, assign) NSInteger idNumber;
#property (nonatomic, weak) id<MyTableViewCellDelegate> delegate;
#end
#protocol MyTableViewCellDelegate <NSObject>
-(void) shouldDeleteFriendById:(NSInteger)idNumber;
#end
And handle the button action inside the MyTableViewCell implementation:
- (void) myInit
{
UIButton* button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
[button addTarget:self action:#selector(onButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
}
- (IBAction)onButtonClicked:(id)sender
{
[self.delegate shouldDeleteFriendById:self.idNumber];
}
Then implement cell's delegate in your tableViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyTableViewCell *cell = (MyTableViewCell*)[tableView dequeueReusableCellWithIdentifier:#"MyCell" forIndexPath:indexPath];
// Configure the cell...
cell.idNumber = indexPath.row;
cell.delegate = self;
return cell;
}
- (void) shouldDeleteFriendById:(NSInteger)idNumber
{
NSLog(#"should delete by id:%d", idNumber);
}

Hide UIButton in Custom UITableViewCell

In my tableview, I have several different custom cells. In one of them, it has a button. This button brings up another view controller. However, It is not needed until the tableview has fully loaded. In cellForRowAtIndexPath I set up all my different custom cells. I can uncomment [buttonCell.myButton setHidden:YES]; and it will hide my button. See below.
else if (indexPath.section == 3)
{
ButtonCell *buttonCell = [tableView dequeueReusableCellWithIdentifier:#"ButtonCell"];
//[buttonCell.myButton setHidden:YES];
cell = buttonCell;
}
return cell;
However, I want to then unhide the button after the tableview loads. I finish loading all my arrays in another method where I call reloadData. In that method, I tried to unhide the button by doing this..
[ButtonCell.myButton setHidden:NO];
But the compiler gives me a warning that property myButton is not found in ButtonCell. Does anyone have any ideas how to go about unhiding my button. What am I doing wrong, and what do I not get! Thanks for all your help.
EDIT 1
My button cell class is...
.h
#import
#interface ButtonCell : UITableViewCell
#property (strong, nonatomic) IBOutlet UIButton *myButton;
- (IBAction)YDI:(id)sender;
#end
.m
#import "ButtonCell.h"
#import "AnotherWebViewController.h"
#implementation ButtonCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (IBAction)YDI:(id)sender
{
}
#end
EDIT 2
With everyone's help that answered (thank you all) I have gotten a bit further, but the button is not showing itself. So I still hide the button in cellForRowAtIndexPath, that works as should. Then in my method that I reload the data in I put the following code.
NSIndexPath *index = [NSIndexPath indexPathForRow:0 inSection:3];
ButtonCell *buttonCell = (ButtonCell *) [self.tableView cellForRowAtIndexPath:index];
[buttonCell.myButton setHidden:NO];
The ButtonCell with the button is always the fourth section (counting the first as 0) and it only has one row. Any other help would be appreciated. Almost there!
EDIT 3
Got it! However, it was due to a comment that I was able to figure it out. Thanks to #A-Live. Although I do know how to get the cell in a method outside of cellForRowAtIndexPath thanks to ElJay. So I am giving him the check since I learned something new which is why we post questions anyway. So inside my method cellForRowAtIndexPath is where I hide/show the button. I have a BOOL in my App called finished, it is originally set to true. When the table view ends loading it is set to false. So I just used this bool to show/hide the button.
else if (indexPath.section == 3)
{
ButtonCell *buttonCell = [tableView dequeueReusableCellWithIdentifier:#"ButtonCell"];
if (!_finished)
{
[buttonCell.myButton setHidden:YES];
}else{
[buttonCell.myButton setHidden:NO];
}
cell = buttonCell;
}
return cell;
Once again this is only part of my cellForRowAtIndexPath method. Thanks once again for all the help. I was surprised to see so many answers! Thanks.
Make the property publicaly accessible.
#property (nonatomic, retain) UIButton *myButton;
Then in cellForRowAtIndexpath
ButtonCell *buttonCell =(ButtonCell *) [tableView dequeueReusableCellWithIdentifier:#"ButtonCell"];
myButton belongs to a cell. You will need to get an instance of that UITableViewCell and then you can unhide it, this assumes you want to modify the cell's objects outside of cellForRowAtIndexPsth or willDisplayCell.
In your code
[ButtonCell.myButton setHidden:NO];
You are trying to use the object class name instead of the object name. You need to get the cell that contains your button
buttonCell = [tableView cellForRowAtIndexPath:indexPath];
buttonCell.myButton.hidden = NO;
Mistake in uppercase maybe ?
[buttonCell.myButton setHidden:NO]; // Trying to access instance variable
Instead of :
[ButtonCell.myButton setHidden:NO]; // Trying to access class variable
Do you have a public accessor for that property in the header file of ButtonCell? Something like #property (nonatomic, retain) UIButton *myButton;
This is how I usually see such a compiler warning.

Resources