iOS unit testing button has action assigned - ios

I've got the following button written programmatically:
self.button = [[UIButton alloc] init];
[self.button setTitle:#"Reverse String!" forState:UIControlStateNormal];
self.button.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1.0];
self.button.layer.cornerRadius = 5.0;
[self.button addTarget:self action:#selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];
However, my unit test fails when I try to test:
-(void)testButtonActionAssigned
{
XCTAssert([self.vc.button respondsToSelector:#selector(reverseString:)]);
}
I get a failed message saying:
test failure: -[ViewControllerTests testButtonActionAssigned] failed:
(([self.vc.button respondsToSelector:#selector(reverseString:)]) is
true) failed
My setup method is called:
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
self.vc = [[ViewController alloc] init];
NSLog(#"THIS METHOD IS CALLED");
}
Is it to do with the simulator timing and startup ?

[self.button addTarget:self action:#selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];
First line means that reverseString: method should be in your ViewController (which is specified by target: parameter and you pass self there). So you need to change test with just removing .button
-(void)testButtonActionAssigned
{
XCTAssert([self.vc respondsToSelector:#selector(reverseString:)]);
}
Here is small example which can be changed:
UIButton *button = [UIButton new];
[button addTarget:self action:#selector(doThat:) forControlEvents:UIControlEventTouchUpInside];
NSString *selectorString = [[button actionsForTarget:self forControlEvent:UIControlEventTouchUpInside] firstObject];
SEL sel = NSSelectorFromString(selectorString);
// so now you can check if target responds to selector
// BOOL responds = [self respondsToSelector:sel];
NSLog(#"%#", selectorString);

I got it. I was testing the wrong thing.
The code in the question isn't really testing the button has an action assigned. It's basically saying "does the view controller responds to this method" or "does the view controller have this method".
// this is saying "does the ViewController know about this method"
// rather than "is ViewController's button assigned method reverseString:"
XCTAssert([self.vc respondsToSelector:...]);
This means regardless of whether button has a selector assigned or not, as long as the method is defined, this assert is always true.
I verified it by commenting out the line of code:
[self.button addTarget:self action:#selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];
and the test still passes, which isn't what I was trying to achieve.
The proper test should be:
-(void)testButtonActionAssigned
{
[self.vc viewDidLoad];
NSArray *arrSelectors = [self.vc.button actionsForTarget:self.vc forControlEvent:UIControlEventTouchUpInside];
NSString *selector = nil;
if(arrSelectors.count > 0)
{
selector = arrSelectors[0];
}
XCTAssert([selector isEqualToString:#"reverseString:"]);
}
Now after commenting out the line:
[self.button addTarget:self action:#selector(reverseString:) forControlEvents:UIControlEventTouchUpInside];
The test fails as expected :D
This proper test code fetches the selector from the button and checks to see if it indeed is the method called reverseString:
EDIT
After B.S. answer, and confirming with Apple's documentation, a shorter version can be written as:
-(void)testButtonActionAssigned
{
[self.vc viewDidLoad];
NSString *selector = [[self.vc.button actionsForTarget:self.vc forControlEvent:UIControlEventTouchUpInside] firstObject];
XCTAssert([selector isEqualToString:#"reverseString:"]);
}
I was originally worried that calling firstObject on an empty array would cause an app to crash but Apple's documentation has confirmed to me that firstObject is a property that returns nil if array is empty.
The first object in the array. (read-only)
Declaration #property(nonatomic, readonly) ObjectType firstObject
Discussion If the array is empty, returns nil.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/#//apple_ref/occ/instp/NSArray/firstObject

Related

Unit testing button click is not working

I have added a button programmatically and I have to run unit test for automation process. We dont have much UI components so we dont use UI testing bundle.
Added button in code
self.proceedButton.frame = CGRectMake(0.0, 0.0, (buttonContainerWidth * 0.75), (buttonContainerHeight * 0.3));
self.proceedButton.center = CGPointMake(self.buttonContainer.center.x, CGRectGetHeight(self.proceedButton.frame));
self.proceedButton.layer.cornerRadius = CGRectGetHeight(self.proceedButton.frame) / 2;
self.proceedButton.layer.masksToBounds = YES;
self.proceedButton.titleLabel.font = proceedFont;
[self.proceedButton addTarget:self action:#selector(onAcceptClicked) forControlEvents:UIControlEventTouchUpInside];
Test:
[vc viewDidLoad];
NSString *selector = [[vc.proceedButton actionsForTarget:vc forControlEvent:UIControlEventTouchUpInside] firstObject];
XCTAssert([selector isEqualToString:#"onAcceptClicked"]);
[vc.proceedButton sendActionsForControlEvents: UIControlEventTouchUpInside];
This fails if I comment out the [self.proceedButton addTarget:self action:#selector(onAcceptClicked) forControlEvents:UIControlEventTouchUpInside]; line so it seems the test is written correctly.
However sendActionsForControlEvents: is not entering onAcceptClicked method in the test.
Why this is happening? Is it possible that unit test is finishing before onAcceptClicked getting actually called?
This could be because you are calling [vc viewDidLoad] directly, which Apple advises you not to do, presumably because a bunch of setup is performed before viewDidLoad is eventually invoked.
You should never call this method directly. The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property.
Instead, try using XCTAssertNotNil(vc.view); at the beginning. This will try to load the view, eventually calling viewDidLoad for you and probably setting up your button so that the actions are correctly added to it.
Edit So if it helps, I've just quickly run this test and I can verify that the method onAcceptClicked is called in the view controller when the test runs.
Button setup in viewDidLoad
CGRect frame = CGRectMake(50, 40, 30, 30);
self.proceedButton = [[UIButton alloc] initWithFrame:frame];
self.proceedButton.titleLabel.text = #"button!!!!";
[self.proceedButton addTarget:self action:#selector(onAcceptClicked) forControlEvents:UIControlEventTouchUpInside];
self.proceedButton.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.proceedButton];
Test
-(void)testButton {
self.myVC = [[UIStoryboard storyboardWithName:#"Main" bundle:nil] instantiateViewControllerWithIdentifier:#"MyVC"];
XCTAssertNotNil(self.myVC.view);
NSString *selector = [[self.myVC.proceedButton actionsForTarget:self.myVC forControlEvent:UIControlEventTouchUpInside] firstObject];
XCTAssert([selector isEqualToString:#"onAcceptClicked"]);
[self.myVC.proceedButton sendActionsForControlEvents: UIControlEventTouchUpInside];
}

Reusing variables in other method file

I have the following code to generate a UIBUtton from a method file called FirstViewController, since the location of the button will change in different secondviewController or thirdViewController, is it possible to set a variable for the location (CGRect) of the unbutton in the FirstViewController and change the CGRect Value in the second or third viewController?
In FirstViewController.m
-(void)method:(UIView *)_view {
UIButton*Touch1= [UIButton buttonWithType:UIButtonTypeRoundedRect];
[Touch1 addTarget:self action:#selector(TouchButton1:) forControlEvents:UIControlEventTouchUpInside];
[Touch1 setFrame:CGRectMake(50,50, 100, 100)];
**//I want to set a variable for the CGRectMake**
Touch1.translatesAutoresizingMaskIntoConstraints = YES;
[Touch1 setBackgroundImage:[UIImage imageNamed:#"1.png"] forState:UIControlStateNormal];
[Touch1 setExclusiveTouch:YES];
[_view addSubview:Touch1];
NSLog(#"test ");
}
In SecondViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
ViewController * ViewCon = [[ViewController alloc]init];
**//Change variable here by defining the new CGRect here.**
[ViewCon method:self.view];
}
This pattern is incorrect:
ViewController * ViewCon = [[ViewController alloc]init];
[ViewCon method:self.view];
as you are allocating a new view controller just to use one of its the method and then you are throwing the view controller instance away. It's extremely inefficient and inconvenient.
Either move the method to a utility class, as a class method:
[Utils method:self.view rect:rect];
or subclass UIViewController and implement the method in that base class and then derive all similar view controllers from that base class, passing any variables into it.
[self method:rect]; // method implemented in MyBaseViewController
You also asked this question before and accepted an answer that promotes the use of this bad pattern. That will mislead others into using this bad pattern.

UIButton problems

So I have been trying to get help with this from other users, but we can never get anywhere, my syntax and code looks good but no matter what I can not get rid of "undecared identifier" error when trying to call my button's method. Im starting to think it is a issue of global/vs not global. Here is my code and my errors
UIButton *add = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[add addTarget:self
action:#selector(aMethod:)forControlEvents:UIControlEventTouchDown];
[add setTitle:#"add new" forState:UIControlStateNormal];
add.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:add];
- (void) aMethod:(id)sender
{
button[0].backgroundColor = [UIColor greenColor];
}
This is all my code for that button in my ViewController.m file... I have 1 warning and 1 error. My warning is
"method definition for 'aMethod' not found"
And this error is tagged under the line "#implementation ViewController" near the top of my code.
My error is
Use of undeclared identifier 'aMethod'
And this is tagged under my "-(void) aMethod: (id)sender"
And I have this in my ViewController.H file
- (void)aMethod;
No erros with that.. I have tried to get help with this before and I keep getting tips and edits that have to do with my programming syntax, But NO MATTER WHAT, i cant get rid of these errors. Is there anything else that could be wrong? Would it be helpful to see the rest of my program? And there is one more piece of info that could be of use. My entire program is written after one of initial lines that xcode sets me up with,
- (void)viewDidLoad
{
//my entire program is between these brackets.
}
When I try this code "-(void) aMethod: (id)sender{ }" before "viewdidload" i dont get an error. but when i put it after "-(void) aMethod: (id)sender{ }" i get the error. I figured that out when i was messing around trying to figure out what's wrong. Let me know if more info is needed. And by the way, i am trying to do it all programmatically, without ever using storyboard...Thank you so much in advance!!
change this in your .h file:
- (void)aMethod;
to this:
- (void)aMethod:(id)sender;
The actual method in your view controller (the .m file) should match this signature.
The "method definition for 'aMethod' not found" is not referring to your button but the fact you have defined the method aMethod in your header but you haven't implemented it in your implementation because you instead have -(void)aMethod:(id)sender;
edit:
Your code should look more like this:
-(void)viewDidLoad {
[super viewDidLoad];
UIButton *add = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[add addTarget:self action:#selector(aMethod:)forControlEvents:UIControlEventTouchDown];
[add setTitle:#"add new" forState:UIControlStateNormal];
add.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:add];
}
- (void) aMethod:(id)sender {
button[0].backgroundColor = [UIColor greenColor];
}

For UIButton how to pass multiple arguments through #selector [duplicate]

This question already has an answer here:
how to pass argument for gesture selector
(1 answer)
Closed 9 years ago.
I need to pass two actions for single UIButton.
First argument passed successfully asa follows:
[imageButton addTarget:self action:#selector(imageClicked:) forControlEvents:UIControlEventTouchUpInside];
imageButton.tag = 1;
But I need to pass another argument also for the same button:
int secondAction =10;
[imageButton addTarget:self action:#selector(imageClicked:*secondAction) forControlEvents:UIControlEventTouchUpInside];
Can anybody help how to pass two values for a single button/selector?
You can use Objective C Runtime feature for associating data with objects as :
Step 1 : Import this in your class : #import <objc/runtime.h>
step 2 : Crete a key name as : static char * kDataAssociationKey = "associated_data_key";
Step 3 : Associate data with your object (Ex: button) as :
NSString *your_data =#"Data which is being associated";
objc_setAssociatedObject(imageButton,
kDataAssociationKey,
your_data,
OBJC_ASSOCIATION_RETAIN);
Step 4 : Get associated data in your method as :
NSString *value = (NSString *)objc_getAssociatedObject(imageButton, kDataAssociationKey);
Hope it helps you.
One argument that the button can receive is (id)sender. This means you can create a new button, inheriting from UIButton, that allows you to store the other intended arguments. Hopefully these two snippets illustrate what to do.
imageButton.tag = 1;
[imageButton addTarget:self action:#selector(buttonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
and
- (IBAction) buttonTouchUpInside:(id)sender {
MyOwnButton *button = (MyOwnButton *)sender; //MyOwnButton inherited from UIButton
or
UIButton *button = (UIButton *) sender;
//do as you please with imageButton.tag
NSLog(#"%d",button.tag);
}
please refer this link for further explanation passing-parameters-on-button-actionselector
Every event has a sender that can be get at selector method by
(void)notify:(NSNotification *)notification {
id notificationSender = [notification object];
//do stuff
}
now this sender is actually an instance whose attributes could be used to get the information about it
now what you can do is you can create a class and add some attribute to it that you want to pass through selector and then use NSNotificationCenter for broad casting your event
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notify:) name:#"Event" object:yourobjectwithattributes];
put this in the class where you want to recieve the event and have the selector
and
[[NSNotificationCenter defaultCenter] postNotificationName:#"Event" object:notificationSender];
this where you want to throw an event
Short answer: You don't. The #selector there tells the button what method to call when it gets tapped, not the arguments that it should pass to the method.
Longer answer: If you know when you're creating the button what the argument is going to be, then you can wrap it up like this:
// In loadView or viewDidLoad or wherever:
[imageButton addTarget:self action:#selector(imageButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
... later ...
- (void)imageButtonTapped:(id)sender
{
[self doStuffToMyImageWithArgument:10];
}
- (void)doStuffToMyImageWithArgument:(NSInteger)argument
{
... do what you gotta do ...
If you don't know, then you probably want to save the argument to a variable somewhere.
// In your #interface
#property (nonatomic, assign) NSInteger imageStuffArgument;
... later ...
// In loadView or viewDidLoad or wherever:
[imageButton addTarget:self action:#selector(imageButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
... later ...
- (void)respondToWhateverChangesTheArgument
{
self.imageStuffArgument = 10;
}
... later ...
- (void)imageButtonTapped:(id)sender
{
[self doStuffToMyImageWithArgument:self.imageStuffArgument];
}
- (void) doStuffToMyImageWithArgument:(NSInteger)argument
{
... do what you gotta do ...

Passing parameters to addTarget:action:forControlEvents

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 :)
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:
with no parameters
action:#selector(switchToNewsDetails)
with 1 parameter indicating the control that sends the message
action:#selector(switchToNewsDetails:)
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:
set a tag property to each button equal to required index
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
}
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.
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);
}
Target-Action allows three different forms of action selector:
- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event
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
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]];
}
}
}
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);
}
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)
}
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.
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
This fixed my problem but it crashed unless I changed
action:#selector(switchToNewsDetails:event:)
to
action:#selector(switchToNewsDetails: forEvent:)
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];
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 ...

Resources