Dynamic UIMenuItems with #selector and dynamic methods - iphone

I am trying to use UIMenuController for a dynamical menu (titles and actions come from a server). The problem is that I have to use UIMenuItems initWithTitle:action: where action is a #selector.
I can use #selector(dispatch:) but then I am not able to distinguish which of the items the user pressed. - (void)dispatch:(id)sender { NSLog(#"%#", sender); } says it is a UIMenuController and It don't have a method which would tell which menu item was pressed.
I can't just write 100 methods to dispatch every possible selector, ok there will not be more then 10 but still, this seems not a good idea.
Do I have to create dynamic methods for each such selector? http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html? This seems odd too.
Any better propositions then this two?
// This approach doesn't work.
- (void)showMenu {
[self becomeFirstResponder];
NSMutableArray *menuItems = [[NSMutableArray alloc] init];
UIMenuItem *item;
for (MLAction *action in self.dataSource.actions) {
item = [[UIMenuItem alloc] initWithTitle:action.title action:#selector(action:)];
[menuItems addObject:item];
[item release];
}
UIMenuController *menuController = [UIMenuController sharedMenuController];
menuController.menuItems = menuItems;
[menuItems release];
[menuController update];
[menuController setMenuVisible:YES animated:YES];
}
- (void)action:(id)sender {
NSLog(#"%#", sender); // gives UIMenuController instead of UIMenuItem
// I can not know which menu item was pressed
}
// This approach is really ugly.
- (void)showMenu {
[self becomeFirstResponder];
NSMutableArray *menuItems = [[NSMutableArray alloc] initWithCapacity:5];
UIMenuItem *item;
NSInteger i = 0;
for (MLAction *action in self.dataSource.actions) {
item = [[UIMenuItem alloc] initWithTitle:action.text
action:NSSelectorFromString([NSString stringWithFormat:#"action%i:", i++])];
[menuItems addObject:item];
[item release];
}
UIMenuController *menuController = [UIMenuController sharedMenuController];
menuController.menuItems = menuItems;
[menuItems release];
[menuController update];
[menuController setMenuVisible:YES animated:YES];
}
- (void)action:(NSInteger)number {
NSLog(#"%i", number); // gives the index of the action in the menu.
}
// This is a hack, I have to assume that there will never be more then 15 actions
- (void)action0:(id)sender { [self action:0]; }
- (void)action1:(id)sender { [self action:1]; }
- (void)action2:(id)sender { [self action:2]; }
- (void)action3:(id)sender { [self action:3]; }
- (void)action4:(id)sender { [self action:4]; }
- (void)action5:(id)sender { [self action:5]; }
- (void)action6:(id)sender { [self action:6]; }
- (void)action7:(id)sender { [self action:7]; }
- (void)action8:(id)sender { [self action:8]; }
- (void)action9:(id)sender { [self action:8]; }
- (void)action10:(id)sender { [self action:10]; }
- (void)action11:(id)sender { [self action:11]; }
- (void)action12:(id)sender { [self action:12]; }
- (void)action13:(id)sender { [self action:13]; }
- (void)action14:(id)sender { [self action:14]; }

That approach would work, although you need a unique selector-name for every button and a mapping from that name to whatever you want to target.
For the selector name a unique string has to be chosen (UUIDs or maybe a sanitized & prefixed version of the title would work). Then you need one method that resolves the call and "alias" it with the different selector names:
- (void)updateMenu:(NSArray *)menuEntries {
Class cls = [self class];
SEL fwd = #selector(forwarder:);
for (MenuEntry *entry in menuEntries) {
SEL sel = [self uniqueActionSelector];
// assuming keys not being retained, otherwise use NSValue:
[self.actionDict addObject:entry.url forKey:sel];
class_addMethod(cls, sel, [cls instanceMethodForSelector:fwd], "v#:#");
// now add menu item with sel as the action
}
}
Now the forwarder can look up what URL is associated with the menu item:
- (void)forwarder:(UIMenuController *)mc {
NSLog(#"URL for item is: %#", [actionDict objectForKey:_cmd]);
}
To generate the selectors you could use something like:
- (SEL)uniqueActionSelector {
NSString *unique = ...; // the unique part
NSString *selString = [NSString stringWithFormat:#"menu_%#:", unique];
SEL sel = sel_registerName([selString UTF8String]);
return sel;
}

Unless the menu items do the same thing, why should they share an action? I would go ahead and write actions that specify a behavior you want and link the menu items up to those.

Related

Checking if a ViewController on Navigation Stack exists

I have this code here to check on the existence of the ViewController. And unfortunately it doesn't work. The thing is, it is executed at the moment of another VC popping from the Navigation Stack:
- (void) leftViewDidHide{
if ([((AppDelegate *)[UIApplication sharedApplication].delegate).frontViewController.navigationController.viewControllers objectAtIndex:1]) {
SGServerListViewController *sample = [[[((AppDelegate *)[UIApplication sharedApplication].delegate).frontViewController.navigationController.viewControllers objectAtIndex:1]childViewControllers] objectAtIndex:0];
[sample.serverTableView setUserInteractionEnabled:YES];
}
}
The app crashes with an exception breakpoint pointing me to the line with an if statement. Any ideas on what could be wrong here? I'm just trying to check if this VC is there and if it is - execute the code.
NSArray *viewControlles = [self.navigationController.viewControllers];
for (int i = 0 ; i <viewControlles.count; i++){
if ([YourVC isKindOfClass:[viewControlles objectAtIndex:i]]) {
//Execute your code
}
}
NSArray *controllerArray = [self.navigationController.viewControllers];
//will get all the controllers added to UINavigationController.
for (id controller in controllerArray)
{
// iterate through the array and check for your controller
if ([controller isKindOfClass:[checkYourController class]])
{
//do your stuff here
}
}
just for an idea containsObject: method of NSArray class might also work.
-(BOOL)isControllerAlreadyOnNavigationControllerStack{
for (UIViewController *vc in self.navigationController.viewControllers) {
if ([vc isKindOfClass:Controller.class]) {
[self.navigationController popToViewController:vc animated:NO];
return YES;
}
}
return NO;
}
if (![self isControllerAlreadyOnNavigationControllerStack]) {
//push controller
}
- (void) leftViewDidHide{
if ([((AppDelegate *)[UIApplication sharedApplication].delegate).frontViewController.navigationController.viewControllers count] == 1) {
SGServerListViewController *sample = [[[((AppDelegate *)[UIApplication sharedApplication].delegate).frontViewController.navigationController.viewControllers objectAtIndex:1]childViewControllers] objectAtIndex:0];
[sample.serverTableView setUserInteractionEnabled:YES];
}
}
You can do that (checking if a ViewController on Navigation Stack exists) with this code:
if navigationController?.viewControllers.count > 1 {
for root in (self.navigationController?.viewControllers)! {
if root is ViewController {
let i = root as? ViewController
i?.table.removeFromSuperview()
}
}
}
self.navigationController?.popToRootViewControllerAnimated(true)
NSArray *viewControlles = self.navigationController.viewControllers;
for (int i = 0 ; i <viewControlles.count; i++){
if ([[viewControlles objectAtIndex:i] isKindOfClass:[RequiredViewController class]]) {
//Enter your code
}
}

UITextField clearButtonMode and UIPicker

I have an extended UITextField (added a NSString property, inputType to it) to take in user input. when the user tap's the text field, i use a picker for the user to select the input. i also need to allow the user to clear the text box so i thought i could use
tillageTextField.clearButtonMode = UITextFieldViewModeAlways;
to clear the text field.
the issue is when you tap the X to clear the text box, it also fires my picker. i use:
-(BOOL) textFieldShouldBeginEditing:(MyTextField *) textField
{
//other code
else if ([textField.inputType isEqualToString:#"tillageMethod"])
{
self.customArray = [NSArray arrayWithObjects:#"No Till", #"Strip Till", #"Full Till", nil];
self.tempTextField = textField;
[self showPicker];
return NO;
}
//other code
}
to show the picker.
is there a way for me to tell if the user tapped the clear button and not show the picker?
You could also implement textFieldShouldClear: and set a flag on your class (e.g. doNotShowPickerOnNextCallback)
- (BOOL)textFieldShouldClear:(UITextField *)textField {
if ([textField.inputType isEqualToString:#"tillageMethod"]) {
if (![self pickerIsShowing]) {
self.doNotShowPickerOnNextCallback = YES;
}
}
return YES;
}
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
//other code
else if ([textField.inputType isEqualToString:#"tillageMethod"]) {
if (self.doNotShowPickerOnNextCallback) {
self.doNotShowPickerOnNextCallback = NO;
return NO;
}
self.customArray = [NSArray arrayWithObjects:#"No Till", #"Strip Till", #"Full Till", nil];
self.tempTextField = textField;
[self showPicker];
return NO;
}
}

how to show UIMenuController to UIBarButtonItem

How to show UIMenuController under UIBarButtonItem when click it?
Assume your UIBarButtonItem has been connected to:
-(void)buttonClicked:(UIBarButtonItem*)sender event:(UIEvent*)event;
Then paste these codes into your view controller:
-(void)buttonClicked:(UIBarButtonItem*)sender event:(UIEvent*)event{
[self becomeFirstResponder];
/*get the view from the UIBarButtonItem*/
UIView *buttonView=[[event.allTouches anyObject] view];
CGRect buttonFrame=[buttonView convertRect:buttonView.frame toView:self.view];
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *resetMenuItem = [[UIMenuItem alloc] initWithTitle:#"Menu Item" action:#selector(menuItemClicked:)];
NSAssert([self becomeFirstResponder], #"Sorry, UIMenuController will not work with %# since it cannot become first responder", self);
[menuController setMenuItems:[NSArray arrayWithObject:resetMenuItem]];
[menuController setTargetRect:buttonFrame inView:self.view];
[menuController setMenuVisible:YES animated:YES];
[resetMenuItem release];
}
- (void) copy:(id) sender {
// called when copy clicked in menu
}
- (void) menuItemClicked:(id) sender {
// called when Item clicked in menu
}
- (BOOL) canPerformAction:(SEL)selector withSender:(id) sender {
if (selector == #selector(menuItemClicked:) /*|| selector == #selector(copy:)*/ /*<--enable that if you want the copy item */) {
return YES;
}
return NO;
}
- (BOOL) canBecomeFirstResponder {
return YES;
}
The key is to return YES for canBecomeFirstResponder and canPerformAction.
Here's the sample project if you need it.
These codes are actually come from other posts in stackoverflow, I just combined them.
Figure out UIBarButtonItem frame in window?
How to get UIMenuController work for a custom view?

iOS: Pop-up menu does not behave in accordance with first responder set

I have several objects inheriting UIView in my application that are tracking taps on them and presenting Copy/Paste pop-up if they contain some specific data. When pop-up is presented I change the appearance of the object as well.
This is how it is implemented:
- (void)viewDidReceiveSingleTap:(NSNotification *)n {
MyObject *mo = (MyObject *)n.object;
[mo becomeFirstResponder];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu update];
[menu setTargetRect:CGRectMake(...) inView:mo];
[menu setMenuVisible:YES animated:YES];
}
MyObject class, in turn, defines canBecomeFirstResponder: and canResignFirstResponder: as always returning YES. becomeFirstResponder:, resignFirstResponder:, and canPerformAction:withSender: is also defined accordingly (this is where I change the appearance of the object).
This is what goes wrong:
I tap object 1. The method viewDidReceiveSingleTap: above is getting called, and the object's canBecomeFirstResponder: and becomeFirstResponder: are getting called too. The pop-up menu is displayed as expected.
I tap another object 2. viewDidReceiveSingleTap: is called again and here's where the trouble starts. First, canResignFirstResponder:, resignFirstResponder: of object 1 are called, but not always, and I can't figure out the pattern. canBecomeFirstResponder: and becomeFirstResponder: of object 2 are called properly, but the pop-up menu does not relocate. It just disappears (though in the viewDidReceiveSingleTap: I clearly call setMenuVisible:YES). In order to make it appear, I have to tap object 2 (or any other) again -- in this case I can see from the debugger that object 2 was set as a first responder, it's just the pop-up that wasn't appearing.
What am I doing wrong? Any clues on relation between pop-up menu visibility and first responder?
The issue is that, when you tap anywhere except on the menu, it does an animated hide of itself. That hide is taking precedence over your animated show. So, if you tap a view to make the menu show, tap anywhere else (either on another one of those views or just anywhere else), and then tap another one of those views, the menu will show every time.
I think there's a good argument to be made for keeping that behavior, because it's the standard behavior that users will expect. But, of course, you have a better idea of what makes sense for your app. So, here's the trick:
[menu setMenuVisible:NO animated:NO];
[menu setMenuVisible:YES animated:YES];
Here's the code I used to try it out:
#implementation MenuView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor blueColor];
}
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self becomeFirstResponder];
UIMenuController *menu = [UIMenuController sharedMenuController];
UIMenuItem *item = [UIMenuItem alloc] initWithTitle:#"Test"
action:#selector(test)];
NSArray *items = [NSArray arrayWithObject:item];
[item release];
[menu setMenuItems:items];
[menu setTargetRect:self.bounds inView:self];
[menu setMenuVisible:NO animated:NO];
[menu setMenuVisible:YES animated:YES];
}
- (void)test {
NSLog(#"Test!");
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
return action == #selector(test);
}
- (BOOL)canBecomeFirstResponder {
NSLog(#"Can become called");
return YES;
}
- (BOOL)canResignFirstResponder {
NSLog(#"Can resign called");
return YES;
}
- (BOOL)becomeFirstResponder {
[super becomeFirstResponder];
NSLog(#"Become called");
return YES;
}
- (void)dealloc {
[super dealloc];
}
#end

Take a random Number from one ViewController and use it in a second ViewController - Update

I have two ViewControllers: The RedButtonViewController and the TweetViewController. The RedButtonViewController generates random numbers in Textlabels and I want to use the number or the label with the TweetViewController. How can I make this?
Thanks for your help!
My TweetViewController will be opened with this code in the RedButtonViewController:
- (IBAction)TweetViewController:(id)sender {
TweetViewController *Tweet = [[TweetViewController alloc] initWithNibName:nil bundle:nil];
[self presentModalViewController:Tweet animated:YES];
}
Here is an exemple of how I generate the random number:
- (IBAction)pushRedButton:(UIButton *)sender {
int ZahlDerToten;
ZahlDerToten = arc4random() % 1000000;
outputLabel.text = [NSString stringWithFormat:#"You killed %d people.", ZahlDerToten];
Create a property on the TweetViewController and set it before presenting it:
- (IBAction)TweetViewController:(id)sender {
// Note: don't put leading capitals on a local variable
TweetViewController *tweet = [[TweetViewController alloc] initWithNibName:nil bundle:nil];
tweet.randomNumber = [self generateRandomNumber];
[self presentModalViewController:tweet animated:YES];
// Note: You were leaking the view controller
[tweet release];
}
Another solution (and how I usually do this kind of thing) is to create a new initializer called -initWithNumber: (probably something a little more descriptive than "number") and call it like this:
- (IBAction)TweetViewController:(id)sender {
TweetViewController *tweet = [[TweetViewController alloc] initWithNumber:[self generateRandomNumber]];
[self presentModalViewController:tweet animated:YES];
[tweet release];
}
-initWithNumber would then look something like:
- (TweetViewController *)initWithNumber:(NSInteger)number {
self = [super initWithNibName:nil bundle:nil];
if (self != nil) {
self.number = number;
}
return self;
}