Can I override the UIControlEventTouchUpInside for a UISegmentedControl? - iphone

I have a UISegmentedControl which I'd like to use to perform a certain action if you click the already selected item.
My thinking is basically something like this:
- (void)viewDidLoad {
UISegmentedControl * testButton = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:#"one", #"two", nil]];
[self.view addSubview:testButton];
[testButton addTarget:self action:#selector(clicked:) forControlEvents:UIControlEventTouchUpInside];
[super viewDidLoad];
}
-(void) clicked: (id) sender{
NSLog(#"click");
}
(And in clicked: I'd just do some check to see if the new selected index is different from the old selected index before the click)
The problem is I can't seem to override the action for the TouchUpInside control event. Any help appreciated!
-S

You can use a subclass to get the behavior you want. Make a subclass of UISegmentedControl that has one BOOL ivar:
BOOL _actionSent;
Then, in the implementation, override the following two methods:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[super sendAction:action to:target forEvent:event];
_actionSent = TRUE;
}
- (void) setSelectedSegmentIndex:(NSInteger)toValue {
_actionSent = FALSE;
[super setSelectedSegmentIndex:toValue];
if (!_actionSent) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
_actionSent = TRUE;
}
}
There are probably other ways but this worked ok for me. I'd be interested to learn of other approaches.

Use UIControlEventValueChanged with UISegmentedControls.

Related

SearchBar disappears from headerview in iOS 7

I have a tableview for showing a list of devices in my application. When viewWillAppear is called, I add the self.searchDisplayController.searchBar as a subview to a headerView. I then assign self.tableView.tableHeaderView = headerView. It looks like this:
I scroll the tableview down so that headerview goes out of view and then go to some other view controller by tapping on a cell. When I come back to this tableView, scroll up to the headerView, the searchBar becomes invisible, however on tapping the invisible area the searchDisplayController gets activated and the cancel button doesn't work. This happens for iOS 7 only. Why is this happening?
Note: It happens only if the headerView is out of the view when I come back to the tableViewController.
I've just had the same issue. When I go to debug into the delegate method of UISearchDisplayController at the end search state, the searchBar becomes a subview of an UIView, not the UITableView. Please see below code:
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller
{
//My Solution: remove the searchBar away from current super view,
//then add it as subview again to the tableView
UISearchBar *searchBar = controller.searchBar;
UIView *superView = searchBar.superview;
if (![superView isKindOfClass:[UITableView class]]) {
NSLog(#"Error here");
[searchBar removeFromSuperview];
[self.tableView addSubview:searchBar];
}
NSLog(#"%#", NSStringFromClass([superView class]));
}
My solution is remove the searchBar away from current super view, then add it as subview again to the tableView. I've already tested successfully.
Hope that help!
Regards
I have the exact same problem. the search bar is still there and can receive touch events. it is however not rendered. I believe the problem is in UISearchDisplaycontroller because it renders fine if I don't use UISearchDisplayController. I ended up writing a custom SearchDisplaycontroller to replace it. it is very basic and only does what I need.
use it is the same way as you would the normal UISearchDisplayController but self.searchDisplayController will not return anything. you will have to use another pointer to refer to the custom search display controller.
looks like a big ugly work around, but the only one that worked for me. keen to hear of alternatives.
#protocol SearchDisplayDelegate;
#interface SearchDisplayController : NSObject<UISearchBarDelegate>
- (id)initWithSearchBar:(UISearchBar *)searchBar contentsController:(UIViewController *)viewController;
#property(nonatomic,assign) id<SearchDisplayDelegate> delegate;
#property(nonatomic,getter=isActive) BOOL active; // configure the view controller for searching. default is NO. animated is NO
- (void)setActive:(BOOL)visible animated:(BOOL)animated; // animate the view controller for searching
#property(nonatomic,readonly) UISearchBar *searchBar;
#property(nonatomic,readonly) UIViewController *searchContentsController; // the view we are searching (often a UITableViewController)
#property(nonatomic,readonly) UITableView *searchResultsTableView; // will return non-nil. create if requested
#property(nonatomic,assign) id<UITableViewDataSource> searchResultsDataSource; // default is nil. delegate can provide
#property(nonatomic,assign) id<UITableViewDelegate> searchResultsDelegate;
#end
#protocol SearchDisplayDelegate <NSObject>
// implement the protocols you need
#optional
#end
the implementation
#implementation SearchDisplayController {
UISearchBar *_searchBar;
UIViewController *_viewController;
UITableView *_searchResultsTableView;
UIView *_overLay;
}
- (void)dealloc {
[_searchBar release];
[_searchResultsTableView release];
[_overLay release];
[super dealloc];
}
- (UIViewController *)searchContentsController {
return _viewController;
}
- (UITableView *)searchResultsTableView {
return _searchResultsTableView;
}
- (id)initWithSearchBar:(UISearchBar *)searchBar contentsController:(UIViewController *)viewController {
self = [super init];
if (self) {
_searchBar = [searchBar retain];
_searchBar.delegate = self;
_viewController = viewController;
_searchResultsTableView = [[UITableView alloc]initWithFrame:CGRectMake(0, CGRectGetMaxY(_searchBar.frame), _viewController.view.frame.size.width, _viewController.view.frame.size.height - CGRectGetMaxY(_searchBar.frame))];
_overLay = [[UIView alloc]initWithFrame:_searchResultsTableView.frame];
_overLay.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(overLayTapped)];
[_overLay addGestureRecognizer:tap];
[tap release];
}
return self;
}
- (void)setSearchResultsDataSource:(id<UITableViewDataSource>)searchResultsDataSource {
_searchResultsTableView.dataSource = searchResultsDataSource;
}
- (void)setSearchResultsDelegate:(id<UITableViewDelegate>)searchResultsDelegate {
_searchResultsTableView.delegate = searchResultsDelegate;
}
- (void)overLayTapped {
[self setActive:NO animated:YES];
[_searchBar resignFirstResponder];
_searchBar.text = nil;
_searchBar.showsCancelButton = NO;
}
- (void)setActive:(BOOL)visible animated:(BOOL)animated {
UIView *viewToAdd = nil;
if (!_searchBar.text.length) {
viewToAdd = _overLay;
} else {
viewToAdd = _searchResultsTableView;
}
float a = 0;
if (visible) {
[_viewController.view addSubview:viewToAdd];
a = 1.0;
}
if ([_viewController.view respondsToSelector:#selectore(scrollEnabled)]) {
((UIScrollView *)_viewController.view).scrollEnabled = !visible;
}
if (animated) {
[UIView animateWithDuration:0.2 animations:^{
_overLay.alpha = a;
_searchResultsTableView.alpha = a;
}];
} else {
_overLay.alpha = a;
_searchResultsTableView.alpha = a;
}
}
- (void)setActive:(BOOL)active {
[self setActive:active animated:YES];
}
#pragma mark - UISearchBar delegate protocols
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
[self setActive:YES animated:YES];
searchBar.showsCancelButton = YES;
[_searchResultsTableView reloadData];
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar {
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
[_searchResultsTableView reloadData];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
[self overLayTapped];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (searchText.length) {
[_overLay removeFromSuperview];
[_viewController.view addSubview:_searchResultsTableView];
} else {
[_searchResultsTableView removeFromSuperview];
[_viewController.view addSubview:_overLay];
}
[_searchResultsTableView reloadData];
}
#end
Update: on how to use this progammatically
declare an ivar
SearchDisplayController *mySearchDisplayController;
initialize it programmatically
mySearchDisplayController = [[SearchDisplayController alloc]initWithSearchBar:mySearchBar contentsController:self];
adding the searchbar to your tableview
self.tableView.headerView = mySearchBar;
use mySearchDisplayController as reference to the custon class instead on self.searchDisplayController.
In my case, the table view that held the search display controller's search bar in its header view was being reloaded almost as soon as the view appeared. It was at this point that the search bar would cease to render. When I scrolled the table, it would reappear. It's also worth mentioning that my table contained a UIRefreshControl and was not a UITableViewController subclass.
My fix involved setting the search display controller active and then inactive very quickly just before after loading the table (and ending the refresh control refreshing):
[self.tableView reloadData];
[self.refreshControl endRefreshing];
[self.searchDisplayController setActive:YES animated:NO];
[self.searchDisplayController setActive:NO];
A bit of a hack but it works for me.
Using the debugger, I've found that the UISearchBar is initially a child view of the tableHeaderView - but when it disappears, it has become a child of the tableView itself. This has probably been done by UISearchDisplayController somehow... So I did the following hack to simply return the UISearchBar to the header view:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.searchDisplayController.isActive && self.searchBar.superview != self.tableView.tableHeaderView) {
[self.tableView.tableHeaderView addSubview:self.searchBar];
}
}
Seems to work fine on iOS 7 as well as 6 :)
(checking that the searchDisplayController isn't active is necessary, otherwise the sarch bar disappears during search)
I endorse Phien Tram's answer. Please upvote it. I don't have enough points myself.
I had a similar problem where a search bar loaded from storyboard would disappear when I repeatedly tapped it, invoking and dismissing search. His solution repairs the problem.
There seems to be a bug where repeated invocation and dismissal of the search display controller doesn't always give the search bar back to the table view.
I will say I'm uncomfortable with the solution's dependence on the existing view hierarchy. Apple seems to reshuffle it with every major release. This code may break with iOS 8.
I think a permanent solution will require a fix by Apple.
I had the same issue and I could fix it calling next line after creating the UISearchDisplayController
[self performSelector:#selector(setSearchDisplayController:) withObject:displayController];
My viewDidLoad function look like this:
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, 44)];
searchBar.placeholder = NSLocalizedString(#"Search", #"Search");
UISearchDisplayController *displayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
displayController.delegate = self;
displayController.searchResultsDataSource = self;
displayController.searchResultsDelegate = self;
[self performSelector:#selector(setSearchDisplayController:) withObject:displayController];
self.tableView.contentOffset = CGPointMake(0, 44);
self.tableView.tableHeaderView = searchBar;
Thanks to this answer: https://stackoverflow.com/a/17324921/1070393
Use UISearchBar above UITableView,Then make IBOutlet for and connect them with file's owner to UISearchbar
Example- .h file
#import <UIKit/UIKit.h>
#interface LocationViewController : UIViewController<UISearchBarDelegate>
{
BOOL IsSearchOn;
}
#property (strong, nonatomic) IBOutlet UISearchBar *searchBar;
#property (strong, nonatomic) IBOutlet UITableView *TBVLocation;
.m file
#pragma mark -
#pragma mark UISearchBar Delegate Methods
-(void) searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[self.searchResult removeAllObjects];
if(searchText.length == 0)
{
IsSearchOn=NO;
// [filteredTableData removeAllObjects];
[self.searchBar resignFirstResponder];
// [self .tblView reloadData];
}
else
{
IsSearchOn=YES;
if(searchText != nil && ![searchText isEqualToString:#""])
{
/* NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:#"SELF contains[c] %#", searchText];
self.searchResult = [NSMutableArray arrayWithArray: [searchArray filteredArrayUsingPredicate:resultPredicate]];*/
for(int i=0;i<[[arrCountryList valueForKey:#"country_name"] count];i++)
{
NSRange titleRange = [[[[arrCountryList valueForKey:#"country_name"] objectAtIndex:i] lowercaseString] rangeOfString:[searchText lowercaseString]];
if(titleRange.location != NSNotFound)
[self.searchResult addObject:[arrCountryList objectAtIndex:i]];
}
[TBVLocation reloadData];
}
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
-(void)searchBarCancelButtonClicked:(UISearchBar *) searchBar
{
[searchBar resignFirstResponder];
IsSearchOn=NO;
searchBar.text = nil;
[TBVLocation reloadData];
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
return YES;
}
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
searchBar.showsCancelButton = YES;
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
// IsSearchOn=YES;
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
IsSearchOn=NO;
searchBar.showsCancelButton = NO;
[TBVLocation reloadData];
[searchBar resignFirstResponder];
}
It will work like charm.
I've faced similar problem and after some digging, I've found that this is a bug in UISearchBar hierarchy. This hacky solution worked for me in iOS 7, but be aware that this may break in future iOS versions:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIView *buggyView = [self.searchDisplayController.searchBar.subviews firstObject];
// buggyView bounds and center are incorrect after returning from controller, so adjust them.
buggyView.bounds = self.searchDisplayController.searchBar.bounds;
buggyView.center = CGPointMake(CGRectGetWidth(buggyView.bounds)/2, CGRectGetHeight(buggyView.bounds)/2);
}
I had the same problem and tested some of the solutions proposed here in this thread, but they didn't solve the problem for me.
Previously, I added and configured the UISearchBar in the
- (void)viewDidLoad
method of my ViewController in code.
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:searchBarView.frame];
searchBar.delegate = self;
searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
ect...
What solved this issue for me was that I added a UISearchbar in the InterfaceBuilder, created an outlet in my ViewController and added this UISearchBar to my UISearchDisplayController.
self.searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar(<--outlet) contentsController:self];
hope this might also help some people

Dismiss view controller from #selector without creating seperate method

I'm trying to dismiss the presented view controller by doing it from the button directly, instead of making a seperate method just for it, but I'm lost on how to get this to work, or if it's even possible.
Any help offered is appreciated!
Code I'm trying:
[dismissButton addTarget:self action:#selector(dismissViewControllerAnimated:YES completion:NULL) forControlEvents:UIControlEventTouchUpInside];
What I'm NOT wanting to do:
- (void)dismissThis
{
[self dismissViewControllerAnimated:YES completion:NULL];
}
It won't work like that. From the documentation of UIControls addTarget:action:forControlEvents::
The action message may optionally include the sender and the event as parameters, in that order.
So you have three possible selectors:
#selector(name)
#selector(nameWithParam:)
#selector(nameWithParam: otherParam:)
if your selector is #selector(dismissViewControllerAnimated:completion:) it will be called with the sender instead of the animated BOOL and the event instead of the completion handler block which will crash you app.
edit to clarify why it crashes:
dismissViewControllerAnimated:completion: copies the completion block by sending the copy message. The event object doesn't implement copy and you will get an NSInvalidArgumentException.
Apple's standard API doesn't support it, but it's easy to add this functionality through a category on UIControl. JTTargetActionBlock adds this functionality. It's also available as a Cocoapod.
[button addEventHandler:^(UIButton *sender, UIEvent *event) {
[self dismissViewControllerAnimated:YES completion:nil];
} forControlEvent:UIControlEventTouchUpInside];
The way I like to handle this is to subclass UIButton and add a block-based action:
#interface BlockButton : UIButton
#property (nonatomic, copy) void (^onPress)();
#end
#implementation BlockButton
-(id) initWithFrame:(CGRect)frame
{
if(self = [super initWithFrame:frame]) {
[self addTarget:self
action:#selector(pressed:)
forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
-(void) pressed:(id)sender
{
if(self.onPress)self.onPress();
}
#end
Then instead of
[dismissButton addTarget:self action:#selector(dismissViewControllerAnimated:YES completion:NULL) forControlEvents:UIControlEventTouchUpInside];
- (void)dismissThis
{
[self dismissViewControllerAnimated:YES completion:NULL];
}
you can use:
dismissButton.onPress = ^{
[self dismissViewControllerAnimated:YES completion:NULL];
};
I'm sure you could adapt this slightly to use a UIButton category instead, if you really don't want a custom button class.
I just created another method in an extension and called dismiss method there.
extension UIViewController {
#objc func dismissAnimated() {
dismiss(animated: true)
}
}
Usage:
let viewController = WebviewController(url: url)
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "Cancel",
style: .plain,
target: viewController,
action: #selector(dismissAnimated))
let navController = UINavigationController(rootViewController: viewController)
let appearance = navController.navigationBar.standardAppearance
appearance.backgroundColor = .white
navController.navigationBar.standardAppearance = appearance
navController.navigationBar.scrollEdgeAppearance = appearance
navController.modalPresentationStyle = .fullScreen
return navController

UISegmentedControl change event not firing in iOS5

I have a UISegmentedControl whose "Value changed" event is wired up in Interface Builder to call my controller's -(IBAction)segmentChangeAction:(id)sender;
When the user taps on the control to change the selected segment, as expected segmentChangeAction is called whether in iOS4 or iOS5.
When I programmatically change the selected segment through segmentedControl.selectedSegmentIndex = newIndex;, on iOS4 segmentChangeAction is called and the segment reflects the new selection. However on iOS5 segmentChangeAction is not called, yet the segment does reflect the new selection.
Is this a change in iOS5? Is there anything I can do to get segmentChangeAction called on iOS5 when I programmatically change the selection?
This is a change in iOS 5 in order for UISegmentedControl to be consistent with all other controls.
The idea is that the action should only fired automatically as a result of user interaction. Prior to iOS 5, UISegmentedControl's actions would be fired because of user interaction and programmatic interaction. However, initiating the change programmatically means that you can also do [myControl sendActionsForControlEvents:UIControlEventValueChanged] yourself.
However, you have to be careful with this. Say you do:
[segmentedControl setSelectedSegmentIndex:newIndex];
[segmentedControl sendActionsForControlEvents:UIControlEventValueChanged];
If you build and run this on iOS 5, it works as you expect. If you build and run this on iOS 4, you'll get your actions fired twice (once when you setSelectedSegmentIndex and again when you sendActions...).
The way around this is to do some sort of guard. This could be a runtime check to indicate that you're running on an iOS 5+ device, or could even be something more mundane, like this:
// changingIndex is a BOOL ivar
changingIndex = YES;
[segmentedControl setSelectedSegmentIndex:newIndex];
changingIndex = NO;
[segmentedControl sendActionsForControlEvents:UIControlEventValueChanged];
and then in your action method...
- (void)segmentedControlSelectedIndexChanged:(id)sender {
if (!changingIndex) {
// your action code here, guaranteed to only run as a result of the sendActions... msg
}
}
I found another way, probably bit easier to understand
you can extend UISegmentedControl and add target action in init methods and call a delegate method to trigger the value change
here is the example code
header file looks like this
#import <UIKit/UIKit.h>
#class CGUISegmentedControl;
#protocol CGUISegmentedControlDelegate <NSObject>
#optional
- (void) segmentedControl:(CGUISegmentedControl *) control valueChangedTo:(NSInteger) nValue;
#end
#interface CGUISegmentedControl : UISegmentedControl
#property (nonatomic,unsafe_unretained) id <CGUISegmentedControlDelegate> delegate;
#end
.m file
#import "CGUISegmentedControl.h"
#implementation CGUISegmentedControl
#synthesize delegate = _delegateAction;
- (void) addTargetAction {
[self addTarget:self action:#selector(indexChanged:) forControlEvents:UIControlEventValueChanged];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addTargetAction];
}
return self;
}
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self addTargetAction];
}
return self;
}
- (id) initWithItems:(NSArray *)items {
self = [super initWithItems:items];
if (self) {
[self addTargetAction];
}
return self;
}
- (id) init {
self = [super init];
if (self) {
[self addTargetAction];
}
return self;
}
- (void) indexChanged:(id) sender {
if (_delegateAction && [_delegateAction respondsToSelector:#selector(segmentedControl:valueChangedTo:)])
[_delegateAction segmentedControl:self valueChangedTo:self.selectedSegmentIndex];
}
#end
And you can set the delegate in the calling class

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

UIMenuController not showing up

I'm trying to create a custom UIMenuController and display it in my view. Here's my code:
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *listMenuItem = [[UIMenuItem alloc] initWithTitle:#"List" action:#selector(addList:)];
[menuController setMenuItems:[NSArray arrayWithObject:listMenuItem]];
[menuController setTargetRect:CGRectMake(50.0, 50.0, 0, 0) inView:self.view];
[menuController setMenuVisible:YES animated:YES];
[listMenuItem release];
There are no errors or exceptions, but the menu controller just doesn't show up.
You need to do three things:
You need to call -becomeFirstResponder on the view or view controller.
Your view or view controller needs to implement -canBecomeFirstResponder (returning YES).
Optionally, your view or view controller can implement -canPerformAction:action withSender:sender to show/hide menu items on an individual basis.
The answer mentions three things, but to be picky, there are six:
The menu handler must be a UIView. If it isn't, -becomeFirstResponder fails.
The menu handler must have userInteractionEnabled = YES
The menu handler must be in the view hierarchy and its -window property must be the same as the window for the view in the inView: argument.
You need to implement -canBecomeFirstResponder and return YES.
You need to call [handler becomeFirstResponder], before [menu setTargetRect:inView:] is called, or the latter will fail.
You need to call [menu setTargetRect:inView] (at least once) and [menu setMenuVisible:animated:].
In particular points 1-3 above got me. I wanted a custom menu handler class that was a UIResponder at first, which caused -becomeFirstResponder to return NO; then it was a UIView, which failed, then I tried making it a UIButton which worked, but only because userInteractionEnabled defaults to YES for buttons and NO for UIViews.
UIMenuController is visible on any view only if the view is first responder and
- (BOOL)canPerformAction method returns YES
Hence if your menu controller is to be shown on button click, the first line in the button action should be [self becomeFirstResponder]. NOTE: here self is the view which will present the menus.
If your menus are to be shown on long press gesture, then add longPressGesture to the UIView and in the longpress event before writing
[menuController setTargetRect:CGRectMake(50.0, 50.0, 0, 0) inView:self.view];
[menuController setMenuVisible:YES animated:YES];
write [self becomeFirstResponder];
Then follow the steps mentioned by OZ.
The below is a full commented working example ...
View subclass header file
#import <Foundation/Foundation.h>
#interface MenuControllerSupportingView : UIView
{
}
#end
View subclass source file
#import "MenuControllerSupportingView.h"
#implementation MenuControllerSupportingView
//It's mandatory and it has to return YES then only u can show menu items..
-(BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)MenuItemAClicked
{
NSLog(#"Menu item A clicked");
}
-(void)MenuItemBClicked
{
NSLog(#"Menu item B clicked");
}
-(void)MenuItemCClicked
{
NSLog(#"Menu item C clicked");
}
//It's not mandatory for custom menu items
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if(action == #selector(MenuItemAClicked))
return YES;
else if(action == #selector(MenuItemBClicked))
return YES;
else if(action == #selector(MenuItemCClicked))
return YES;
else
return NO;
}
view Controller header file
#import <UIKit/UIKit.h>
#interface ViewController1 : UIViewController
#end
view Controller source file
#import "ViewController1.h"
#import "MenuControllerSupportingView.h"
#interface ViewController1 ()
{
MenuControllerSupportingView *vu;
}
#end
#implementation ViewController1
- (void)viewDidLoad
{
[super viewDidLoad];
vu=[[SGGI_MenuControllerSupportingView alloc]initWithFrame:CGRectMake(0,0,768,1024)];
[self.view addSubview:vu];
UIButton *btn=[UIButton buttonWithType:UIButtonTypeCustom];
[btn setFrame:CGRectMake(200,200,200,30)];
[btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[btn setTitle:#"Show" forState:UIControlStateNormal];
[btn addTarget:self action:#selector(SHowMenu) forControlEvents:UIControlEventTouchUpInside];
[vu addSubview:btn];
}
-(void)SHowMenu
{
UIMenuController *menucontroller=[UIMenuController sharedMenuController];
UIMenuItem *MenuitemA=[[UIMenuItem alloc] initWithTitle:#"A" action:#selector(MenuItemAClicked)];
UIMenuItem *MenuitemB=[[UIMenuItem alloc] initWithTitle:#"B" action:#selector(MenuItemBClicked)];
UIMenuItem *MenuitemC=[[UIMenuItem alloc] initWithTitle:#"C" action:#selector(MenuItemCClicked)];
[menucontroller setMenuItems:[NSArray arrayWithObjects:MenuitemA,MenuitemB,MenuitemC,nil]];
//It's mandatory
[vu becomeFirstResponder];
//It's also mandatory ...remeber we've added a mehod on view class
if([vu canBecomeFirstResponder])
{
[menucontroller setTargetRect:CGRectMake(10,10, 0, 200) inView:vu];
[menucontroller setMenuVisible:YES animated:YES];
}
}
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
#end
In View class if u write return YES alone in canPerformAction you will see all the default menuitems like camera symbol,cut,copy etc..
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
return YES;
}
if u want to show something like camera alone then
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if(action==#selector(_insertImage:))
return YES;
else
return NO;
}
if u want to know about all the actions then
visit the link
Just in case anyone is having this issue specifically (and randomly) with iOS6: you might want to look at this SO related to having Speak Selection enabled on the device (Settings -> General -> Accessibility -> Speak Selection: On). A small number of my users were not able to see the custom UIMenuItems and this was the cause.
In Swift 3.0 -
In my case I wanted to have the VC pre-select the text in a TextView and display a custom menu for the user to take action on that selection. As mentioned by Kalle, order is very important, especially making setMenuVisible last.
In VC, viewDidLoad:
menuCont = UIMenuController.shared
let menuItem1: UIMenuItem = UIMenuItem(title: "Text", action: #selector(rtfView.textItem(_:)))
let menuItems: NSArray = [menuItem1]
menuCont.menuItems = menuItems as? [UIMenuItem]
In VC, when the user hits a button:
#IBAction func pressed(_ sender: Any) {
self.textView.selectedRange = NSMakeRange(rangeStart, rangeLength)
self.textView.becomeFirstResponder()
menuCont.setTargetRect(CGRect.zero, in: self.textView)
menuCont.setMenuVisible(true, animated: true)
}
Finally, in the sub-class of the TextView:
class rtfView: UITextView {
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any!) -> Bool {
if (action == #selector(textItem(_:))) {
return true
} else {
return false
}
}
}
maybe because CGRectMake(50.0, 50.0, 0, 0) creates a CGRect with width = 0 and height = 0?
cheers,
anka