129

I am using addTarget:action:forControlEvents like this:

[newsButton addTarget:self
action:@selector(switchToNewsDetails)
forControlEvents:UIControlEventTouchUpInside];

and I would like to pass parameters to my selector "switchToNewsDetails". The only thing I succeed in doing is to pass the (id)sender by writing:

action:@selector(switchToNewsDetails:)

But I am trying to pass variables like integer values. Writing it this way doesn't work :

int i = 0;
[newsButton addTarget:self
action:@selector(switchToNewsDetails:i)
forControlEvents:UIControlEventTouchUpInside];

Writing it this way does not work either:

int i = 0;
[newsButton addTarget:self
action:@selector(switchToNewsDetails:i:)
forControlEvents:UIControlEventTouchUpInside];

Any help would be appreciated :)

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Pierre Espenan
  • 3,996
  • 5
  • 33
  • 51

13 Answers13

175
action:@selector(switchToNewsDetails:)

You do not pass parameters to switchToNewsDetails: method here. You just create a selector to make button able to call it when certain action occurs (touch up in your case). Controls can use 3 types of selectors to respond to actions, all of them have predefined meaning of their parameters:

  1. with no parameters

    action:@selector(switchToNewsDetails)
    
  2. with 1 parameter indicating the control that sends the message

    action:@selector(switchToNewsDetails:)
    
  3. With 2 parameters indicating the control that sends the message and the event that triggered the message:

    action:@selector(switchToNewsDetails:event:)
    

It is not clear what exactly you try to do, but considering you want to assign a specific details index to each button you can do the following:

  1. set a tag property to each button equal to required index
  2. in switchToNewsDetails: method you can obtain that index and open appropriate deatails:

    - (void)switchToNewsDetails:(UIButton*)sender{
        [self openDetails:sender.tag];
        // Or place opening logic right here
    }
    
Vladimir
  • 170,431
  • 36
  • 387
  • 313
  • 4
    what if we're trying to pass something other than integers? what if I need to pass an object like a string? – user102008 Apr 06 '11 at 22:55
  • @user what's your context? Seems you'll need to pass it separately – Vladimir Apr 07 '11 at 08:17
  • thanks a lot. where did you find this data? I always appreciate the reference so that maybe I could find it myself next time. – bearMountain Mar 30 '12 at 16:33
  • 1
    @bearMountain you can check this link:https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaFundamentals/CommunicatingWithObjects/CommunicateWithObjects.html#//apple_ref/doc/uid/TP40002974-CH7-SW44 for example – Vladimir Mar 30 '12 at 20:05
  • I get an error trying to use @selector "expected , separator" – quantumpotato Oct 28 '14 at 23:00
  • @quantumpotato, that looks weird. what's your actual code is? – Vladimir Oct 30 '14 at 16:53
  • I need something similar: I need to pass the indexpath.row AND the indexpath.section to be passed. How to achieve this? – Joris416 Jan 10 '15 at 14:26
  • 4
    You don't need to pass it separately. If you are passing any NSObject you can just say `[button.layer setValue:yourObject forKey:@"anyKey"];` and then in the method just check `(objectClass *)[button.layer valueForKey:@"anyKey"];` It's like a more free version of `.tag` – Albert Renshaw Oct 14 '16 at 20:40
64

To pass custom params along with the button click you just need to SUBCLASS UIButton.

(ASR is on, so there's no releases in the code.)

This is myButton.h

#import <UIKit/UIKit.h>

@interface myButton : UIButton {
    id userData;
}

@property (nonatomic, readwrite, retain) id userData;

@end

This is myButton.m

#import "myButton.h"
@implementation myButton
@synthesize userData;
@end

Usage:

myButton *bt = [myButton buttonWithType:UIButtonTypeCustom];
[bt setFrame:CGRectMake(0,0, 100, 100)];
[bt setExclusiveTouch:NO];
[bt setUserData:**(insert user data here)**];

[bt addTarget:self action:@selector(touchUpHandler:) forControlEvents:UIControlEventTouchUpInside];

[view addSubview:bt];

Recieving function:

- (void) touchUpHandler:(myButton *)sender {
    id userData = sender.userData;
}

If you need me to be more specific on any part of the above code — feel free to ask about it in comments.

Yuriy Polezhayev
  • 812
  • 8
  • 12
  • I think this is the best way – Cagatay Gurturk Jan 12 '14 at 16:24
  • Most elegant solution. – Victor C. Feb 07 '14 at 12:56
  • 2
    Great answer...very customizable. Exactly what I was looking for. MOLODETS! :) – denikov Mar 27 '14 at 00:17
  • thanks for the feedback :) Glad, that it is useful for somebody :) – Yuriy Polezhayev Mar 27 '14 at 08:36
  • I have a button needed to customize in xib file. How can I make this button as mybutton programmatically? – ttotto May 06 '15 at 03:28
  • 1
    Simple and useful workaround. Just let me add that this is similar to the [`tag` property in Android views](http://developer.android.com/intl/es/reference/android/view/View.html#setTag(java.lang.Object)). They can store any object. And you can also [store objects by key](http://developer.android.com/intl/es/reference/android/view/View.html#setTag(int,%20java.lang.Object)) like in a Dictionary/Map. – Ferran Maylinch Dec 04 '15 at 14:26
21

Need more than just an (int) via .tag? Use KVC!

You can pass any data you want through the button object itself (by accessing CALayers keyValue dict).


Set your target like this (with the ":")

[myButton addTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];

Add your data(s) to the button itself (well the .layer of the button that is) like this:

NSString *dataIWantToPass = @"this is my data";//can be anything, doesn't have to be NSString
[myButton.layer setValue:dataIWantToPass forKey:@"anyKey"];//you can set as many of these as you'd like too!

*Note: The key shouldn't be a default key of a CALayer property, consider adding a unique prefix to all of your keys to avoid any issues arising from key collision.


Then when the button is tapped you can check it like this:

-(void)buttonTap:(UIButton*)sender{

    NSString *dataThatWasPassed = (NSString *)[sender.layer valueForKey:@"anyKey"];
    NSLog(@"My passed-thru data was: %@", dataThatWasPassed);

}
Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
  • there is no method in UIButton.layer to set or add an object. Where do you get that solution ? – mAc May 07 '17 at 19:49
  • 1
    A UIButton is a type of UIView, all UIViews have CALayer .layer property. CALayer contains NSDictionary behaviors. Note this is Objective-C not Swift, that may be the issue? Here are Apple's Docs on CALayer key-value coding: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html – Albert Renshaw May 07 '17 at 20:10
  • I am doing in Objective-C only, but when u try to write addObject for UIButton compiler says no method for UIbutton. Have tried to do it in Xcode ? – mAc May 08 '17 at 04:07
  • @mAc My apologies, it's `setValue:` not `addObject:`. Correcting my code above now – Albert Renshaw May 08 '17 at 04:22
  • That's fine, I did the other way but I liked your answer better than others in this post. +1 – mAc May 08 '17 at 16:20
  • 1
    This should be the answer on top. By far the easiest way to do it!. thanks! – danielrosero Sep 18 '18 at 06:15
  • 1
    It should be accepted answer.Its much easier and smart way. – Emon Apr 17 '19 at 06:56
  • 1
    Great answer! Wish I'd known this a long time ago. – John Apr 30 '19 at 17:33
19

Target-Action allows three different forms of action selector:

- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event
Benoît
  • 7,395
  • 2
  • 25
  • 30
8

I made a solution based in part by the information above. I just set the titlelabel.text to the string I want to pass, and set the titlelabel.hidden = YES

Like this :

UIButton *imageclick = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
imageclick.frame = photoframe;
imageclick.titleLabel.text = [NSString stringWithFormat:@"%@.%@", ti.mediaImage, ti.mediaExtension];
imageclick.titleLabel.hidden = YES;

This way, there is no need for a inheritance or category and there is no memory leak

Abhinav Singh
  • 7,880
  • 1
  • 23
  • 33
5

I was creating several buttons for each phone number in an array so each button needed a different phone number to call. I used the setTag function as I was creating several buttons within a for loop:

for (NSInteger i = 0; i < _phoneNumbers.count; i++) {

    UIButton *phoneButton = [[UIButton alloc] initWithFrame:someFrame];
    [phoneButton setTitle:_phoneNumbers[i] forState:UIControlStateNormal];

    [phoneButton setTag:i];

    [phoneButton addTarget:self
                    action:@selector(call:)
          forControlEvents:UIControlEventTouchUpInside];
}

Then in my call: method I used the same for loop and an if statement to pick the correct phone number:

- (void)call:(UIButton *)sender
{
    for (NSInteger i = 0; i < _phoneNumbers.count; i++) {
        if (sender.tag == i) {
            NSString *callString = [NSString stringWithFormat:@"telprompt://%@", _phoneNumbers[i]];
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:callString]];
        }
    }
}
rjdunwoody91
  • 63
  • 1
  • 4
  • you can improve this code: `- (void)call:(UIButton *)sender { NSString *phoneNumber = _phoneNumbers[i]; NSString *callString = [NSString stringWithFormat:@"telprompt://%@", phoneNumber]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:callString]]; }` – Ladessa Jul 31 '14 at 13:05
2

As there are many ways mentioned here for the solution, Except category feature .

Use the category feature to extend defined(built-in) element into your customisable element.

For instance(ex) :

@interface UIButton (myData)

@property (strong, nonatomic) id btnData;

@end

in the your view Controller.m

 #import "UIButton+myAppLists.h"

UIButton *myButton = // btn intialisation....
 [myButton set btnData:@"my own Data"];
[myButton addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];

Event handler:

-(void)buttonClicked : (UIButton*)sender{
    NSLog(@"my Data %@", sender. btnData);
}
Kumar KL
  • 15,315
  • 9
  • 38
  • 60
2

You can replace target-action with a closure (block in Objective-C) by adding a helper closure wrapper (ClosureSleeve) and adding it as an associated object to the control so it gets retained. That way you can pass any parameters.

Swift 3

class ClosureSleeve {
    let closure: () -> ()

    init(attachTo: AnyObject, closure: @escaping () -> ()) {
        self.closure = closure
        objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
    }

    @objc func invoke() {
        closure()
    }
}

extension UIControl {
    func addAction(for controlEvents: UIControlEvents, action: @escaping () -> ()) {
        let sleeve = ClosureSleeve(attachTo: self, closure: action)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
    }
}

Usage:

button.addAction(for: .touchUpInside) {
    self.switchToNewsDetails(parameter: i)
}
Marián Černý
  • 15,096
  • 4
  • 70
  • 83
  • Above method ( `self.switchToNewsDetails(parameter: i)`) firing 2 times , why help me out – Nagendar Jan 04 '18 at 05:31
  • The code works for me. Tested in a new Single View App project https://pastebin.com/tPaqetMb. So the problem is probably in your code, like calling `button.addAction()` twice. Add a breakpoint to `button.addAction` and also on the first line inside the closure and try to debug it yourself. – Marián Černý Jan 04 '18 at 13:34
2

There is another one way, in which you can get indexPath of the cell where your button was pressed:

using usual action selector like:

 UIButton *btn = ....;
    [btn addTarget:self action:@selector(yourFunction:) forControlEvents:UIControlEventTouchUpInside];

and then in in yourFunction:

   - (void) yourFunction:(id)sender {

    UIButton *button = sender;
    CGPoint center = button.center;
    CGPoint rootViewPoint = [button.superview convertPoint:center toView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:rootViewPoint];
    //the rest of your code goes here
    ..
}

since you get an indexPath it becames much simplier.

1

See my comment above, and I believe you have to use NSInvocation when there is more than one parameter

more information on NSInvocation here

http://cocoawithlove.com/2008/03/construct-nsinvocation-for-any-message.html

Aaron Saunders
  • 33,180
  • 5
  • 60
  • 80
1

This fixed my problem but it crashed unless I changed

action:@selector(switchToNewsDetails:event:)                 

to

action:@selector(switchToNewsDetails: forEvent:)              
Sangram Shivankar
  • 3,535
  • 3
  • 26
  • 38
iphaaw
  • 6,764
  • 11
  • 58
  • 83
0

I subclassed UIButton in CustomButton and I add a property where I store my data. So I call method: (CustomButton*) sender and in the method I only read my data sender.myproperty.

Example CustomButton:

@interface CustomButton : UIButton
@property(nonatomic, retain) NSString *textShare;
@end

Method action:

+ (void) share: (CustomButton*) sender
{
    NSString *text = sender.textShare;
    //your work…
}

Assign action

    CustomButton *btn = [[CustomButton alloc] initWithFrame: CGRectMake(margin, margin, 60, 60)];
    // other setup…

    btnWa.textShare = @"my text";
    [btn addTarget: self action: @selector(shareWhatsapp:)  forControlEvents: UIControlEventTouchUpInside];
iNiko84
  • 105
  • 1
  • 6
0

If you just want to change the text for the leftBarButtonItem shown by the navigation controller together with the new view, you may change the title of the current view just before calling pushViewController to the wanted text and restore it in the viewHasDisappered callback for future showings of the current view.

This approach keeps the functionality (popViewController) and the appearance of the shown arrow intact.

It works for us at least with iOS 12, built with Xcode 10.1 ...

Bubu
  • 1