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
Related
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
I am showing a some text and images in UIwebview in navigation based application. It also contains some link to the source of the data i.e the websites. It is showing it in perfect manner. But when user click on the back button it pop the previous viewcontroller and goes to it. But, What I want when user click on the link the back button must be converted into browser's back button and act like it.
Any suggestions/sample code or tutorial for it?
You can replace the navigation bar's back button with a browser back button whenever the UIWebView has the option to go back by doing something like this:
- (void)updateBackButton {
if ([self.webView canGoBack]) {
if (!self.navigationItem.leftBarButtonItem) {
[self.navigationItem setHidesBackButton:YES animated:YES];
UIBarButtonItem *backItem = [[[UIBarButtonItem alloc] initWithTitle:#"Back" style:UIBarButtonItemStylePlain target:self action:#selector(backWasClicked:)] autorelease];
[self.navigationItem setLeftBarButtonItem:backItem animated:YES];
}
}
else {
[self.navigationItem setLeftBarButtonItem:nil animated:YES];
[self.navigationItem setHidesBackButton:NO animated:YES];
}
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
[self updateBackButton];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self updateBackButton];
}
- (void)backWasClicked:(id)sender {
if ([self.webView canGoBack]) {
[self.webView goBack];
}
}
See First thing that you have to do is, that you have to hide the default back button, as follows below
self.navigationItem.hidesBackButton = YES;
After this you have to add a custom back button on the navigationbar as below:
UIButton *m_BackBtn = [UIButton buttonWithType:UIButtonTypeCustom];
m_BackBtn.frame = CGRectMake(4.0, 5.0+0.0, 100, 30);
[m_BackBtn setTitle:#"Back" forState:UIControlStateNormal];
[m_BackBtn addTarget:selfaction:#selector(BackButtonAction)forControlEvents:UIControlEventTouchUpInside];
[m_BackBtn retain];
[self.navigationController.navigationBar addSubview:m_BackBtn];
*And in the BackButtonAction Function *
-(void)BackButtonAction
{
[yourwebView goBack];
// Do your stuff as you required
}
Take a UIBarbuttonItem from XIB at the place of back button and you
can write following in its IBAction method Suppose your UIWebview
object is
UIWebView *web;
-(IBAction)goBack
{
[web goBack];
}
goBack is inbuilt method of UIWebView
I like #cduhn's approach above for it's simplicity, but it has the downside of losing the "<" on the Back button. See this answer if you want to keep the "real" back button (with it's "<" icon), but change the button's behavior:
https://stackoverflow.com/a/19132881/132542
For the web view history case here, this is the code I used in my delegate method:
-(BOOL) navigationShouldPopOnBackButton {
if (webView.canGoBack) {
// Then just go back in the web view
[webView goBack];
// return NO to keep the current view controller loaded
return NO;
}
else {
// no web history, so pop the view
return YES;
}
}
This is a bit tricky solution, may not be suggested. But this way you can maintain the back navigation animation on navigation bar.
var urlRequest: NSURLRequest?
////////
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == .LinkClicked {
//AViewController creates and pushes itself to the navigationController
let pushViewController = self.storyboard?.instantiateViewControllerWithIdentifier("AViewController") as! AViewController
pushViewController.urlRequest = request
self.navigationController?.pushViewController(pushViewController, animated: true)
return false
} else { //is initial request from push
return true
}
}
I have a usual editButtonItem in my navigationBar (created by the system), and I'd like to change its name. Si I wrote this lines in my TableViewController :
- (void)viewDidLoad
{
[super viewDidLoad];
[Some code...]
self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.navigationItem.rightBarButtonItem.title = #"New name";
}
That works, but when entering and exiting the Edit Mode, its system name is restored.
I tried to force it again in didEndEditingRowAtIndexPath for example, but with no success...
What should I do to fix this custom name without having to build the button from start by myself in the code ?
#Bojan's method is close but very inefficient, since it gets called for every single row. Just change the title at the beginning on viewDidLoad and in addition do this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
// Make sure you call super first
[super setEditing:editing animated:animated];
if (editing)
{
self.editButtonItem.title = NSLocalizedString(#"Cancel", #"Cancel");
}
else
{
self.editButtonItem.title = NSLocalizedString(#"Edit", #"Edit");
}
}
Create your own edit button item if you don't like the name of the existing one.
Declare an ivar editItem in your header then create the item like so:
editItem = [[UIBarButtonItem alloc] initWithTitle:#"New Name" style:UIBarButtonItemStyleBordered target:self action:#selector(toggleEditing)]
in -toggleEditing, call
[self setEditing:!self.editing animated:YES]
and also update the title of the button (and optionally the appearance) to reflect the editing state.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.tableView.editing) {
self.editButtonItem.title = #"CustomDoneName";
}
else
self.editButtonItem.title = #"CustomEditName";
return YES;
}
This works fine for me.
To supplement these answers. Custom is the way to go BUT keep in mind:
If you do create your own button, which will change title, make sure you set it's possibleTitles property.
self.myEditButton.possibleTitles = [NSSet setWithObjects:#"Edit Seats", #"Done", nil];
Otherwise, the transition animation on your button may get weird, especially if the titles differ in length significantly. The reason for this is, we change the button's title as it's animating being tapped on.
Here is the only way I found out to fix the temporary shrunk dirty transition state of the editButtonItem when toggling the button with custom titles.
Even setting possibleTitles does not provide a perfect result.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
NSString *editButtonTitleBefore = self.editButtonItem.title;
[super setEditing:editing animated:animated];
[self.tableView setEditing:editing animated:animated];
[self setToolbarLayout];
if (editing && ![self.editButtonItem.title isEqualToString:NSLocalizedString(#"Cancel", #"Cancel")])
{
self.editButtonItem.title = NSLocalizedString(#"Cancel", #"Cancel");
}
else if (!editing && ![editButtonTitleBefore isEqualToString:#"Edit"])
{
// Dirty hax to avoid the editButton system title label shrink bug
self.editButtonItem.title = #"";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.editButtonItem.title = NSLocalizedString(#"Edit", #"Edit");
});
}
}
For Swift 3 you should override the setEditing(_ editing: Bool, animated: Bool) UIViewController's method as bellow:
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if self.isEditing {
self.editButtonItem.title = "[New editing title]"
}
else {
self.editButtonItem.title = "[New to edit title]"
}
}
Furthermore, you should call this method on viewDidLoad to the editButtonItem starts on the view with the correct title, as bellow:
override func viewDidLoad() {
super.viewDidLoad()
self.setEditing(false, animated: false)
self.navigationItem.leftBarButtonItem = self.editButtonItem
}
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
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.