Strange behaviour of dynamically added UIBarButtonItem - ios

I'm working on an application in which I'm adding UIBarButtonItem to UIToolbar dynamically. When User clicks on a bar button. I'm changing it's tint color to red.
But for some bar buttons it's not working and the application is crashing.
Here is my code:
#interface myClass : UIViewController
#property (nonatomic, retain) NSMutableArray *barButtonItems;
#property (nonatomic, retain) IBOutlet UIToolbar *toolBar;
#end
#implementation myClass
#sythesize barButtonItems, toolBar;
- (void)viewDidLoad
{
[super viewDidLoad];
barButtonItems = [[NSMutableArray alloc] init];
[self initToolBar];
}
//To set the tool bar
- (void)initToolBar
{
[self addBarItem:#"PlantDetails" actionName:#"createPlantDetails:"];
[self addBarItem:#"ElectricalEquipmentInventory" actionName:#"createInventory:button:"];
toolBar.items = barButtonItems;
}
//Create bar button item
- (void)addBarItem:(NSString*)barButtonName actionName:(NSString*)methodName
{
UIBarButtonItem *plantDetails = [[UIBarButtonItem alloc] initWithTitle:barButtonName style:UIBarButtonItemStyleDone target:self action:NSSelectorFromString(methodName)];
[barButtonItems addObject:plantDetails];
[plantDetails release];
plantDetails = nil;
}
//Changes the barbutton tintcolor when user selected
-(void)changeSelection:(UIBarButtonItem *)button
{
NSArray *tempArray = toolBar.items;
for(int loop = 0; loop<[tempArray count]; loop++)
[[tempArray objectAtIndex:loop] setTintColor:[UIColor blackColor]];
[button setTintColor:[UIColor redColor]];
}
//First bar button method
- (void)createPlantDetails:(UIBarButtonItem *)button
{
[self changeSelection:button];
NSLog(#"createPlantDetails");
}
//second bar button method
- (void)createInventory:(int)selectedIndex button:(UIBarButtonItem *)button
{
[self changeSelection:button];
NSLog(#"createInventory");
}
#end
Here my issue is the bar button with only one parameter in it's selector is working perfectly (createPlantDetails) but when I click on the bar button which have two parameter in it's selector (createInventory) the application is crashing on [button setTintColor:[UIColor redColor]]; of changeSelection method.
Crash log is something like: touches event have no method like setTintColor .
I searched a lot but couldn't find a solution. Please help me.
Thanks in advance

The method for the action property has to have one of the following three forms:
- (void)methodName;
- (void)methodName:(id)sender;
- (void)methodName:(id)sender withEvent:(UIEvent *)event;
You can't use any arbitrary format or custom parameters (the button wouldn't know what to pass for them).
The createPlantDetails: method works because it matches the second form.
The createInventory:button: method fails because it doesn't match any of the expected signatures.
Since your method has two parameters, when the button calls the method, the button passes a UIEvent object in the second parameter which in your method is named button.
In changeSelection:, it crashes when it tries to call setTintColor: because button is really a UIEvent and not the UIBarButtonItem (ie. the sender).

Related

iOS Objective-C UIBarButtonItem Action Not Called

I have a property:
#property (strong, nonatomic) IBOutlet UIBarButtonItem *backButton;
It gets set like this:
UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:#selector(goBack:)];
backItem.imageInsets = UIEdgeInsetsZero;
UIToolbar *backToolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
backToolbar.userInteractionEnabled = YES;
[backToolbar setTransform:CGAffineTransformMakeScale(-1, 1)];
backToolbar.items = [NSArray arrayWithObjects:backItem, nil];
backToolbar.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0];
backToolbar.clipsToBounds = YES;
self.backButton = [[UIBarButtonItem alloc] initWithCustomView:backToolbar];
self.backButton.enabled = YES;
self.backButton.imageInsets = UIEdgeInsetsZero;
Which I would have thought would set the selector to this:
- (IBAction)goBack:(id)sender {
NSLog(#"goBack pushed");
[self.iframeView goBack];
}
But goBack pushed is never logged out when I click the button, though the iframeView (just a UIWebView) does go to the previous page.
I've also tried setting the action like this (set right below the above code):
[self.backButton setTarget:self];
[self.backButton setAction:#selector(goBack:)];
Any ideas how I can adjust the code such that the selector is called when I push the back button?
Did you create your backButton from storyboard (because you declare it as an IBOutlet)?
If you already create it from storyboard then you can simply Ctrl-Left Click to drag the button into your implementation file .m where the - (IBAction)goBack:(id)sender is to create the selector.
UPDATE:
I just noticed you said this
But goBack pushed is never logged out when I click the button, though
the iframeView (just a UIWebView) does go to the previous page.
UIWebView has the goBack function already. So by setting
[self.backButton setAction:#selector(goBack:)];
When the button is pressed it will called the goBack function of the UIWebView.
All you have to is give it a different name:
[self.backButton setAction:#selector(backButtonPressed:)];
- (IBAction)backButtonPressed:(id)sender{
NSLog(#"goBack pushed");
[self.iframeView goBack];
}
I tried your code and I found a line of code is missing which is why the selector goBack: is not triggered. If I am not wrong the button "Play" is not visible when you compile
self.navigationItem.rightBarButtonItem = self.backButton;
As I observed you have created the bar button, toolbar programmatically, also you don't need the #property (strong, nonatomic) IBOutlet UIBarButtonItem *backButton; if your not using it else where in the code or you have to omit the IBOutlet.
Hope these helps.

Access UIButton in a category of a UIViewController

I am creating a simple category of UIViewController which adds the possibility to show a button simply on top of the view to display a chat window.
#interface UIViewController (ChatButton)
- (void)showChatButtonFromTop;
- (void)showChatButtonFromBottom;
- (void)hideButton;
#end
Now in the method I create a button and display it:
- (void)showChatButtonFromTop
{
UIButton* chatBtn = [self constructButtonWithWidth:buttonAxisSize X:buttonX Y:buttonY];
[self.view addSubview:chatBtn];
}
But I also need to hide the button:
- (void)hideButton
{
// confusion!
}
How do I get that button? Categories don't allow for properties, so how do I store the reference?
when you add the button to view, add a tag to button
chatBtn.tag = 1234;
[self.view addSubview:chatBtn];
when you try to hide the button access it's tag
- (void)hideButton
{
UIButton *chatBtn = (UIButton *)[self.view viewWithTag:1234];
[chatBtn setHidden:YES];
}

Issues with UINavigationItem changing properties at runtime

i have a Navigation Bar, wich contains a Navigation Item, which contains 2 Bar Buttons, these are created in the Storyboard, and i wanted to change 1 of the buttons at runtime, now this works:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UINavigationItem *thisNavBar = [self myNavigationItem];
thisNavBar.rightBarButtonItem = nil; // this works, it gets removed
UIBarButtonItem *insertBtn = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:#selector(insertSkemaItem:)];
thisNavBar.rightBarButtonItem = insertBtn; // this also works, it sets the btn
}
Now, in my other method, which is called by another controller, it does not work
- (void)callChildChange {
...
// remove old btn
UINavigationItem *thisNavBar = [self skemaNavigationItem];
thisNavBar.rightBarButtonItem = nil; // does not work?
}
There is nothing wrong with the method, it runs just fine, but the nav btn item does not get removed ?
skemaNavigationItem is a Navigation item, declared in the .h file which links the navigation item i made via the storyboard.
Your UI items need to be added to your code (by ctrl-dragging) in the header file (.h) so they can be publicly accessed from other classes/view controllers.
Presuming you've done this, hiding a UI item is best done by using
relevantClass.yourViewObject.hidden = YES;
or if you really need to delete it for good,
[relevantClass.yourViewObject.view removeFromSuperView];
Edits
Options for changing target method:
Declare #property (nonatomic, assign) BOOL myButtonWasPressed; and:
- (IBAction) myButtonPressed
{
if (!self.myButtonWasPressed)
{
// This code will run the first time the button is pressed
self.myButton.text = #"New Button Text";
self.myButtonWasPressed = YES;
}
else
{
// This code will run after the first time your button is pressed
// You can even set your BOOL property back, and make it toggleable
}
}
or
- (IBAction) myButtonWasPressedFirstTime
{
// do what you need to when button is pressed then...
self.myButton.text = #"New Button Text";
[self.myButton removeTarget:self action:#selector(myButtonPressedFirstTime) forControlEvents:UIControlEventTouchUpInside];
[self.myButton addTarget:self action:#selector(myButtonPressedAgain) forControlEvents: UIControlEventTouchUpInside];
}
- (IBAction) myButtonWasPressedAgain
{
// this code will run the subsequent times your button is pressed
}

iPad undo button (a-la Keynote and other apps)

In Keynote (and other apps), I've noticed the "standard" interface of doing Undo/Redo is by providing an Undo button on the tool bar.
Clicking the button (that is always enabled) Undos the recent operation.
(If there is not recent operation to undo, it will show the Undo/Redo menu).
Long-clicking the Undo button opens an Undo/Redo menu.
I searched for methods of implementing this, and the best answer I found so far is at the following link.
I wonder if anyone knows of a simpler way?
Thanks!
After reviewing all methods and discussing with friends, below is the solution I used, for a UIBarButtonItem the responds to both taps and long-press (TapOrLongPressBarButtonItem).
It is based on the following principals:
Subclass UIBarButtonItem
Use a custom view (so it's really trivial to handle the long-press - since our custom view has no problem responding to a long-press gesture handler...)
... So far - this approach was in the other SO thread - and I didn't like this approach since I couldn't find and easy enough way of making the custom view appear like an iPad navigation bar button... Soooo...
Use UIGlossyButton by Water Lou (thanks water!). This use is encapsulated within the subclass...
The resulting code is as follows:
#protocol TapOrPressButtonDelegate;
#interface TapOrPressBarButtonItem : UIBarButtonItem {
UIGlossyButton* _tapOrPressButton;
__weak id<TapOrPressButtonDelegate> _delegate;
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate;
#end
#protocol TapOrPressButtonDelegate<NSObject>
- (void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
- (void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
#end
#implementation TapOrPressBarButtonItem
- (void)buttonLongPressed:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if([_delegate respondsToSelector:#selector(buttonLongPressed:withBarButtonItem:)]) {
[_delegate buttonLongPressed:_tapOrPressButton withBarButtonItem:self];
}
}
- (void)buttonTapped:(id)sender {
if (sender != _tapOrPressButton) {
return;
}
if([_delegate respondsToSelector:#selector(buttonTapped:withBarButtonItem:)]) {
[_delegate buttonTapped:_tapOrPressButton withBarButtonItem:self];
}
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate {
if (self = [super init]) {
// Store delegate reference
_delegate = delegate;
// Create the customm button that will have the iPad-nav-bar-default appearance
_tapOrPressButton = [UIGlossyButton buttonWithType:UIButtonTypeCustom];
[_tapOrPressButton setTitle:title forState:UIControlStateNormal];
[_tapOrPressButton setNavigationButtonWithColor:[UIColor colorWithRed:123.0/255 green:130.0/255 blue:139.0/255 alpha:1.0]];
// Calculate width...
CGSize labelSize = CGSizeMake(1000, 30);
labelSize = [title sizeWithFont:_tapOrPressButton.titleLabel.font constrainedToSize:labelSize lineBreakMode:UILineBreakModeMiddleTruncation];
_tapOrPressButton.frame = CGRectMake(0, 0, labelSize.width+20, 30);
// Add a handler for a tap
[_tapOrPressButton addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// Add a handler for a long-press
UILongPressGestureRecognizer* buttonLongPress_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(buttonLongPressed:)];
[_tapOrPressButton addGestureRecognizer:buttonLongPress_];
// Set this button as the custom view of the bar item...
self.customView = _tapOrPressButton;
}
return self;
}
// Safe guards...
- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithCustomView:(UIView *)customView {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
#end
And all you need to do is:
1. Instantiate is as follows:
TapOrPressBarButtonItem* undoMenuButton = [[TapOrPressBarButtonItem alloc] initWithTitle:NSLocalizedString(#"Undo", #"Undo Menu Title") andDelegate:self];
2. Connect the button to the navigation bar:
[self.navigationItem setLeftBarButtonItem:undoMenuButton animated:NO];
3. Implement the TapOrPressButtonDelegate protocol, and you're done...
-(void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self menuItemUndo:barButtonItem];
}
-(void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self undoMenuClicked:barButtonItem];
}
Hope this helps anyone else...
If you are using IB (or in Xcode4 the designer...i guess it is called) then you can select "Undo" from the First responder and drag that action to a button. I can give you more specific instructions if that doesn't cover it.
Here's what it looks like
It's on the left underneath the column "Received actions" at the bottom
I believe the key is actually in the UINavigationBar itself. Unlike UIButtons or other normal touch tracking objects, I suspect UIBarItems don't handle their own touches. They don't inherit UIResponder or UIControl methods. However UINavigationBar of course does. And I've personally added gestures straight to a UINavigationBar many times.
I suggest you override touch handling in a UINavigationBar subclass and check the touches against its children. If the child is your special Undo button you can handle it accordingly.
UIButton* undoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[undoButton addTarget:self action:#selector(undoPressStart:) forControlEvents:UIControlEventTouchDown];
[undoButton addTarget:self action:#selector(undoPressFinish:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* navButton = [[[UIBarButtonItem alloc] initWithCustomView:undoButton] autorelease];
self.navigationItem.rightBarButtonItem = navButton;
You don't necessarily have to add the UIBarButtonItem as the rightBarButtonItem, this is just and easy way to show you how to create your UIBarButtonItem with a custom view that is the UIButton you want to handle events.
You'll need to implement the undoPressStart: and undoPressFinish: by maintaining state. I'd say on start, store the current NSDate or some granular representation of the time. On finish, if check the time elapsed and if it is beyond a certain threshold, show the menu - otherwise (as well as if the start date was never captured) perform the undo.
As an improvement, you'll likely want to observe the UIControlEventTouchDragExit event as well to cancel the long press.

iOS - passing Sender (button) name to addSubview

I have a main view with 3 buttons. Clicking on any of the buttons adds a SubView.
The buttons have different titles and are all linked to IBAction "switchView"
The "switchView" code is below.
- (IBAction)switchView:(id)sender{
secondView *myViewController = [[secondView alloc] initWithNibName:#"secondView" bundle:nil];
[self.view addSubview:myViewController.view];
}
The "secondView" loads up correctly and everything works well.
The problem is I want to be able to know which button was the Sender.
I don't want to create 3 subviews, one for each button. The code and XIB would be absolutely the same>
The only difference would be a variable that I would like to set up in the second view (viewDidLoad method) depending on who is the Sender (which button was clicked)
Is this possible? Or I would need to create 3 subViews - one for each button?
Your help is greatly appreciated!
You can identify different buttons with the tag property.
e.g. with your method:
-(IBAction)switchView:(id)sender {
UIButton *button = (UIButton*)sender;
if (button.tag == 1) {
//TODO: Code here...
} else if (button.tag == 2) {
//TODO: Code here...
} else {
//TODO: Code here...
}
}
The tag property can be set via the InterfaceBuilder.
Hope this helps.
I think you can solve in 2 ways:
Create a property like:
#property (nonatomic, strong) IBOutlet UIButton *button1, *button2, *button3;
in your viewcontroller and link the buttons to them as referencing outlet on the XIB.
Give a different tag to each button on your xib and ask for the tag of the sender with UIButton *b=(UIButton*)sender; b.tag; like Markus posted in detail.
Solving my problem it all came down to transferring data between the mainView and subView.
In my mainView.h I declared an NSString and its #property
...
NSString *btnPressed;
}
#property(nonatomic, retain) NSString *btnPressed;
...
then in my mainView.m inside the switchView method I did this:
- (IBAction)switchView:(id)sender{
secondView *myViewController = [[secondView alloc] initWithNibName:#"secondView" bundle:nil];
btnPressed = [NSString stringWithFormat:#"%i", [sender tag]];
[myViewController setBtnPressed:self.btnPressed];
[self.view addSubview:myViewController.view];
}
This line in the code above actually takes care of transferring the data to the newly created subView:
[myViewController setBtnPressed:self.btnPressed];
Then in my secondView.h I declare exactly the same NSString *btnPressed and its #property (though this a completely different object than the one declared in main)
Then in my secondView.m I get the value of the button pressed I'm interested in.
- (void)viewDidLoad {
[super viewDidLoad];
int theValueOfTheButtonPressed = [self.btnPressed intValue];
}
This works well.
Don't forget to #synthesize btnPressed; as well as [btnPressed release]; in both mainView.m and secondView.m

Resources