Pull Dismiss ViewController Without BackBarButton [duplicate] - ios

I have an iOS 7 app where I am setting a custom back button like this:
UIImage *backButtonImage = [UIImage imageNamed:#"back-button"];
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[backButton setImage:backButtonImage forState:UIControlStateNormal];
backButton.frame = CGRectMake(0, 0, 20, 20);
[backButton addTarget:self
action:#selector(popViewController)
forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *backBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
viewController.navigationItem.leftBarButtonItem = backBarButtonItem;
But this disables the iOS 7 "swipe left to right" gesture to navigate to the previous controller. Does anyone know how I can set a custom button and still keep this gesture enabled?
EDIT:
I tried to set the viewController.navigationItem.backBarButtonItem instead, but this doesn't seem to show my custom image.

IMPORTANT:
This is a hack. I would recommend taking a look at this answer.
Calling the following line after assigning the leftBarButtonItem worked for me:
self.navigationController.interactivePopGestureRecognizer.delegate = self;
Edit:
This does not work if called in init methods. It should be called in viewDidLoad or similar methods.

Use the backIndicatorImage and backIndicatorTransitionMaskImage properties of the UINavigationBar if at all possible. Setting these on an a UIAppearanceProxy can easily modify behavior across your application. The wrinkle is that you can only set those on ios 7, but that works out because you can only use the pop gesture on ios 7 anyway. Your normal ios 6 styling can remain intact.
UINavigationBar* appearanceNavigationBar = [UINavigationBar appearance];
//the appearanceProxy returns NO, so ask the class directly
if ([[UINavigationBar class] instancesRespondToSelector:#selector(setBackIndicatorImage:)])
{
appearanceNavigationBar.backIndicatorImage = [UIImage imageNamed:#"back"];
appearanceNavigationBar.backIndicatorTransitionMaskImage = [UIImage imageNamed:#"back"];
//sets back button color
appearanceNavigationBar.tintColor = [UIColor whiteColor];
}else{
//do ios 6 customization
}
Trying to manipulate the interactivePopGestureRecognizer's delegate will lead to a lot of issues.

I saw this solution http://keighl.com/post/ios7-interactive-pop-gesture-custom-back-button/ which subclasses UINavigationController. Its a better solution as it handles the case where you swipe before the controller is in place - which causes a crash.
In addition to this I noticed if you do a swipe on the root view controller (after pushing on one, and back again) the UI becomes unresponsive (also same problem in answer above).
So the code in the subclassed UINavigationController should look like so:
#implementation NavigationController
- (void)viewDidLoad {
[super viewDidLoad];
__weak NavigationController *weakSelf = self;
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.interactivePopGestureRecognizer.delegate = weakSelf;
self.delegate = weakSelf;
}
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
// Hijack the push method to disable the gesture
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.interactivePopGestureRecognizer.enabled = NO;
}
[super pushViewController:viewController animated:animated];
}
#pragma mark - UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animate {
// Enable the gesture again once the new controller is shown
self.interactivePopGestureRecognizer.enabled = ([self respondsToSelector:#selector(interactivePopGestureRecognizer)] && [self.viewControllers count] > 1);
}
#end

I use
[[UINavigationBar appearance] setBackIndicatorImage:[UIImage imageNamed:#"nav_back.png"]];
[[UINavigationBar appearance] setBackIndicatorTransitionMaskImage:[UIImage imageNamed:#"nav_back.png"]];
[UIBarButtonItem.appearance setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -64) forBarMetrics:UIBarMetricsDefault];

Here is swift3 version of Nick H247's answer
class NavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
if responds(to: #selector(getter: interactivePopGestureRecognizer)) {
interactivePopGestureRecognizer?.delegate = self
delegate = self
}
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if responds(to: #selector(getter: interactivePopGestureRecognizer)) {
interactivePopGestureRecognizer?.isEnabled = false
}
super.pushViewController(viewController, animated: animated)
}
}
extension NavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
interactivePopGestureRecognizer?.isEnabled = (responds(to: #selector(getter: interactivePopGestureRecognizer)) && viewControllers.count > 1)
}
}
extension NavigationController: UIGestureRecognizerDelegate {}

I also hide the back button, replacing it with a custom leftBarItem.
Removing interactivePopGestureRecognizer delegate after push action worked for me:
[self.navigationController pushViewController:vcToPush animated:YES];
// Enabling iOS 7 screen-edge-pan-gesture for pop action
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
}

navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
This is from http://stuartkhall.com/posts/ios-7-development-tips-tricks-hacks, but it causes several bugs:
Push another viewController into the navigationController when swiping in from the left edge of the screen;
Or, swipe in from the left edge of the screen when the topViewController is popping up from the navigationController;
e.g. When the rootViewController of navigationController is showing, swipe in from the left edge of the screen, and tap something(QUICKLY) to push anotherViewController into the navigationController, then
The rootViewController does not respond any touch event;
The anotherViewController will not be shown;
Swipe from the edge of the screen again, the anotherViewController will be shown;
Tap the custom back button to pop the anotherViewController, crash!
So you must implement UIGestureRecognizerDelegate method in self.navigationController.interactivePopGestureRecognizer.delegate like this:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer == navigationController.interactivePopGestureRecognizer) {
return !navigationController.<#TODO: isPushAnimating#> && [navigationController.viewControllers count] > 1;
}
return YES;
}

Try self.navigationController.interactivePopGestureRecognizer.enabled = YES;

I did not write this, but the following blog helped a lot and solved my issues with custom navigation button:
http://keighl.com/post/ios7-interactive-pop-gesture-custom-back-button/
In summary, he implements a custom UINavigationController that uses the pop gesture delegate. Very clean and portable!
Code:
#interface CBNavigationController : UINavigationController <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
#end
#implementation CBNavigationController
- (void)viewDidLoad
{
__weak CBNavigationController *weakSelf = self;
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)])
{
self.interactivePopGestureRecognizer.delegate = weakSelf;
self.delegate = weakSelf;
}
}
// Hijack the push method to disable the gesture
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)])
self.interactivePopGestureRecognizer.enabled = NO;
[super pushViewController:viewController animated:animated];
}
#pragma mark UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animate
{
// Enable the gesture again once the new controller is shown
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)])
self.interactivePopGestureRecognizer.enabled = YES;
}
Edit. Added fix for problems when a user tries to swipe left on a root view controller:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if ([self respondsToSelector:#selector(interactivePopGestureRecognizer)] &&
self.topViewController == [self.viewControllers firstObject] &&
gestureRecognizer == self.interactivePopGestureRecognizer) {
return NO;
}
return YES;
}

RootView
override func viewDidAppear(_ animated: Bool) {
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
ChildView
override func viewDidLoad() {
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
}
extension ChildViewController: UIGestureRecognizerDelegate {}

Use this logic to keep enable or disable the swipe gesture..
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animate
{
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)])
{
if (self.navigationController.viewControllers.count > 1)
{
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
else
{
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
}
}

I had a similar problem where I was assigning the current view controller as the delegate for the interactive pop gesture, but would break the gesture on any views pushed, or views underneath the view in the nav stack. The way I solved this was to set the delegate in -viewDidAppear, then set it to nil in -viewWillDisappear. That allowed my other views to work correctly.

Imagine we are using Apple's default master/detail project template, where master is a table view controller and tapping on it will show the detail view controller.
We want to customize the back button that appears in the detail view controller. This is how to customize the image, image color, text, text color, and font of the back button.
To change the image, image color, text color, or font globally, place the following in a location that is called before any of your view controllers are created (e.g. application:didFinishLaunchingWithOptions: is a good place).
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UINavigationBar* navigationBarAppearance = [UINavigationBar appearance];
// change the back button, using default tint color
navigationBarAppearance.backIndicatorImage = [UIImage imageNamed:#"back"];
navigationBarAppearance.backIndicatorTransitionMaskImage = [UIImage imageNamed:#"back"];
// change the back button, using the color inside the original image
navigationBarAppearance.backIndicatorImage = [[UIImage imageNamed:#"back"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
navigationBarAppearance.backIndicatorTransitionMaskImage = [UIImage imageNamed:#"back"];
// change the tint color of everything in a navigation bar
navigationBarAppearance.tintColor = [UIColor greenColor];
// change the font in all toolbar buttons
NSDictionary *barButtonTitleTextAttributes =
#{
NSFontAttributeName: [UIFont fontWithName:#"HelveticaNeue-Light" size:12.0],
NSForegroundColorAttributeName: [UIColor purpleColor]
};
[[UIBarButtonItem appearance] setTitleTextAttributes:barButtonTitleTextAttributes forState:UIControlStateNormal];
return YES;
}
Note, you can use appearanceWhenContainedIn: to have more control over which view controllers are affected by these changes, but keep in mind that you can't pass [DetailViewController class], because it is contained inside a UINavigationController, not your DetailViewController. This means you will need to subclass UINavigationController if you want more control over what is affected.
To customize the text or the font/color of a specific back button item, you must do so in the MasterViewController (not the DetailViewController!). This seems unintuitive because the button appears on the DetailViewController. However once you understand that the way to customize it is by setting a property on a navigationItem, it begins to make more sense.
- (void)viewDidLoad { // MASTER view controller
[super viewDidLoad];
UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithTitle:#"Testing"
style:UIBarButtonItemStylePlain
target:nil
action:nil];
NSDictionary *barButtonTitleTextAttributes =
#{
NSFontAttributeName: [UIFont fontWithName:#"HelveticaNeue-Light" size:12.0],
NSForegroundColorAttributeName: [UIColor purpleColor]
};
[buttonItem setTitleTextAttributes:barButtonTitleTextAttributes forState:UIControlStateNormal];
self.navigationItem.backBarButtonItem = buttonItem;
}
Note: attempting to set the titleTextAttributes after setting self.navigationItem.backBarButtonItem doesn't seem to work, so they must be set before you assign the value to this property.

Create a class 'TTNavigationViewController' which is subclass of 'UINavigationController' and make your existing navigation controller of this class either in storyboard/class, Example code in class -
class TTNavigationViewController: UINavigationController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.setNavigationBarHidden(true, animated: false)
// enable slide-back
if self.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)) {
self.interactivePopGestureRecognizer?.isEnabled = true
self.interactivePopGestureRecognizer?.delegate = self
}
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}}

Related

iOS Push to another view controller get stuck

I have an base view controller and all my viewcontrollers inherit it.
#interface BaseViewController () <UIGestureRecognizerDelegate>
#end
#implementation BaseViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.tabBarController.tabBar.translucent = NO;
self.navigationController.navigationBar.translucent = NO;
self.navigationController.interactivePopGestureRecognizer.delegate = self;
self.view.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.backButton];
self.navigationItem.backBarButtonItem.title = #"";
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleDefault;
}
- (void) popToPreViewController {
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark - getter and setter
- (UIButton *) backButton {
if (!_backButton) {
_backButton = [[UIButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 20.0, 20.0)];
[_backButton addTarget:self action:#selector(popToPreViewController) forControlEvents:UIControlEventTouchUpInside];
[_backButton setImage:[UIImage imageNamed:#"main_back"] forState:UIControlStateNormal];
_backButton.hidden = YES;
}
return _backButton;
}
#end
Sometimes push to another view controller will get stuck ,but the app does not crash.Press home button and open the app again, it shows the another view controller . Is there something wrong with this baseviewcontroller ?
I have been troubled by this problem for a long time, now find a solution.
The reason for this problem is that the root controller performs a gesture pop operation, the problem described above occurs. The workaround is to disable the gesture pop at the root controller and open the gesture pop on the non root controller.
/// The controller is fully displayed, opening the gesture pop
Func navigationController (_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
If viewControllers.first == viewController {
InteractivePopGestureRecognizer? .isEnabled = false
} Else {
InteractivePopGestureRecognizer? .isEnabled = true
}
NavigationBar.sendSubview (toBack: navigationBar.overlay)
}

navigation title on previous screen disappear when back button pressed

i have a problem with navigation bar title. i have 2 screen, when i click back button on screen 2 it will back to screen 1 but the title of screen 1 is disappear. Here is my snippet code
screen1 .m
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = #"title 1";
....
screen 2 .m
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = self.stringTitle;
// change the back button and add an event handler
self.navigationItem.hidesBackButton = YES;
//set custom image to button if needed
UIImage *backButtonImage = [UIImage imageNamed:#"btn_back.png"];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:backButtonImage forState:UIControlStateNormal];
button.frame = CGRectMake(-12, 2, backButtonImage.size.width, backButtonImage.size.height);
[button addTarget:self action:#selector(backAction) forControlEvents:UIControlEventTouchUpInside];
UIView *backButtonView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, backButtonImage.size.width-15, backButtonImage.size.height)];
[backButtonView addSubview:button];
UIBarButtonItem *customBarItem = [[UIBarButtonItem alloc] initWithCustomView:backButtonView];
self.navigationItem.leftBarButtonItem = customBarItem;
self.navigationController.navigationBar.titleTextAttributes = #{NSForegroundColorAttributeName: [UIColor whiteColor]};
....
}
- (void) backAction
{
[self.navigationController popViewControllerAnimated:YES];
}
i've tried several tricks to make title of screen 1 appears with the following tricks:
set title on viewwillappear method
- (void)viewWillAppear:(BOOL)animated{self.title = #"tes";}
set title on viewdidappear method
- (void)viewDidAppear:(BOOL)animated{
self.navigationController.navigationBar.topItem.title = #"YourTitle";
self.title = #"tes";
}
but the title is still disappears. please help
here is the way how i moving to next screen
- (IBAction)btGenerateTokenAction:(id)sender {
SofttokenResultController *controller = [[SofttokenResultController alloc] initWithNibName:#"SofttokenResultController" bundle:nil];
controller.navigationController.navigationBar.backItem.title = #"Kembali";
[ViewLoadingUtil animateToNextView:controller from:self :#""];
}
+ (void) animateToNextView:(UIViewController *)controller from:(UIViewController *)fromController :(NSString *)navTitle{
NSLog(#"animateToNextView called");
[fromController.navigationController pushViewController:controller animated:YES];
}
This solved my problem:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated: animated)
self.navigationItem.title="Title text"
}
In my case, it was the backItem?.title that was the problem. In both my view controller and within a custom subclass of UINavigationController I was setting it to an empty string:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
...
navigationController.navigationBar.backItem?.title = ""
}
My guess would be that when using popViewController(animated:) the title is taken from backItem?.title and set into the main title attribute.
In viewWillAppear implement this:
self.navigationItem.title="Title text";
it usually happen when you set backItem?.title = "" because the title of the back controller is taken from this property
I face this issue from months and solved it just by commenting this line
Working for me solution. You can try this one:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.topItem?.title = ""
self.navigationItem.title = "Title"
}
Objective c solution:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear: animated];
self.navigationItem.title = #"Title text";
}
I come from the Swift world. However I tried many solutions. These were caused by the animation. If you change the Bool to false, it will solve your problem. Finally you can put your code in the viewWillAppear function.
override func viewWillAppear(animated: Bool) {
if animated {
//...
}
}

self.navigationItem.hidesBackButton = YES and go back with swipe gesture on iOS7

I write the codes below in my UIViewController that uses UINavigationController.
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.hidesBackButton = YES;
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>) self;
}
I build and run my app,
self.navigationItem.hidesBackButton = YES;
above that works correctly, but
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>) self;
that one DO NOT works.
So, I re-write the code below.
- (void)viewDidLoad {
UIBarButtonItem *backBarButton = [[UIBarButtonItem alloc] initWithCustomView:[[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 44.0f, 44.0f)]];
backBarButton.tintColor = [UIColor clearColor];
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
self.navigationItem.leftBarButtonItem = backBarButton;
}
It works correctly.
However, I want to use the first example.
The first one clearly express what I want to do.
Does someone have any idea?
In viewDidLoad, the view controller is not yet contained in a navigation controller, so the navigationController property is nil, which is why that line has no effect.
That said, assigning the delegate of UINavigationController's interactivePopGestureRecognizer is not good practice (I'm pretty sure it expects to be assigned to the navigation controller). Try disabling the gesture recognizer in viewWillAppear: instead:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

iOS 7 change backbarbuttonitem text [duplicate]

How can I customize the navigation back button in iOS 7 and above without title? (i.e. with the arrow only)
self.navigationItem.leftBarButtonItem = self.editButtonItem;
I'm just wondering if they have any self.backButtonItem;
OR
something like this?
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemBACK
target:self action:#selector(back)];
It's actually pretty easy, here is what I do:
Objective C
// Set this in every view controller so that the back button displays back instead of the root view controller name
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"" style:UIBarButtonItemStylePlain target:nil action:nil];
Swift 2
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .Plain, target: nil, action: nil)
Swift 3
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
Put this line in the view controller that is pushing on to the stack (the previous view controller). The newly pushed view controller back button will now show whatever you put for initWithTitle, which in this case is an empty string.
I found an easy way to make my back button with iOS single arrow.
Let's supouse that you have a navigation controller going to ViewA from ViewB. In IB, select ViewA's navigation bar, you should see these options: Title, Prompt and Back Button.
ViewA navigate bar options
The trick is choose your destiny view controller back button title (ViewB) in the options of previous view controller (View A). If you don't fill the option "Back Button", iOS will put the title "Back" automatically, with previous view controller's title. So, you need to fill this option with a single space.
Fill space in "Back Button" option
The Result:
Just use an image!
OBJ-C:
- (void)viewDidLoad {
[super viewDidLoad];
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:#"Icon-Back"]
style:UIBarButtonItemStylePlain
target:self.navigationController
action:#selector(popViewControllerAnimated:)];
self.navigationItem.leftBarButtonItem = backButton;
}
SWIFT 4:
let backBTN = UIBarButtonItem(image: UIImage(named: "Back"),
style: .plain,
target: navigationController,
action: #selector(UINavigationController.popViewController(animated:)))
navigationItem.leftBarButtonItem = backBTN
navigationController?.interactivePopGestureRecognizer?.delegate = self
Icon-Back.png
Icon-Back#2x.png
Icon-Back#3x.png
iOS7 has new interface rules, so It's better to keep at least the back arrow when you push a UIView.
It's very easy to change the "back" text programmatically. Just add this code before push the view (Or prepareForSegue if you are using StoryBoards):
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
self.navigationItem.backBarButtonItem=[[UIBarButtonItem alloc] initWithTitle:#"NEW TITLE" style:UIBarButtonItemStylePlain target:nil action:nil];
}
This will change the default "Back" text, but will keep the iOS7 styled back arrow.
You can also change the tint color for the back arrow before push the view:
- (void)viewDidLoad{
//NavBar background color:
self.navigationController.navigationBar.barTintColor=[UIColor redColor];
//NavBar tint color for elements:
self.navigationController.navigationBar.tintColor=[UIColor whiteColor];
}
Hope this helps you!
Nothing much you need to do. You can achieve the same through storyboard itself.
Just go the root Navigation controller and give a space. Remember not to the controller you wanted the back button without title, but to the root navigation controller.
While Kyle Begeman's answer totally does the trick, it is quite annoying to have this code in every view controller possible. I ended up with a simple UINavigationItem category. Beware, here be dragons! Sorry, I mean, swizzling:
#import <objc/runtime.h>
#implementation UINavigationItem (ArrowBackButton)
static char kArrowBackButtonKey;
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method m1 = class_getInstanceMethod(self, #selector(backBarButtonItem));
Method m2 = class_getInstanceMethod(self, #selector(arrowBackButton_backBarButtonItem));
method_exchangeImplementations(m1, m2);
});
}
- (UIBarButtonItem *)arrowBackButton_backBarButtonItem {
UIBarButtonItem *item = [self arrowBackButton_backBarButtonItem];
if (item) {
return item;
}
item = objc_getAssociatedObject(self, &kArrowBackButtonKey);
if (!item) {
item = [[UIBarButtonItem alloc] initWithTitle:#"" style:UIBarButtonItemStyleBordered target:nil action:NULL];
objc_setAssociatedObject(self, &kArrowBackButtonKey, item, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return item;
}
#end
This works, but it will remove the title of the previous item, even if you pop back to it:
self.navigationController.navigationBar.topItem.title = #"";
Just set this property on viewDidLoad of the pushed View Controller.
EDIT: 2014-04-09: As I gained reputations, I feel sorry because I don't use this trick any more. I recommend Kyle's answer. Also notice that the self of self.navigationItem.backBarButtonItem isn't the view controller the back button is displayed, but the previous view controller to be went back.
If you don't need to have title text for the previous view controller, just fill the title with a blank string;
self.navigationItem.title = #"";
[self.navigationController pushViewController:viewController animated:YES];
This will prevent showing "back" with chevron on the pushed view controller.
EDIT: Even you use non-blank title text, setting the title of the previous view controller in viewWillAppear: works except the title can flicker in a blink when view controller popped. I think "The twitter app" seems to do more subtle hack to avoid the flicker.
This is how I do it and the simplest, works and most clear way to do it.
This works if embed on Navigation Controller
Swift 3
In viewDidLoad I add this to the View Controller you want the back button to be just arrow.
if let topItem = self.navigationController?.navigationBar.topItem {
topItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
The difference of this to #Kyle Begeman's answer is that you call this on the view controller that you want the back button to be just arrow, not on the pushing stack view controller.
You don't have access to the navigation backButtonItem with the way you want, you need to create your own back button like below:
- (void)loadView
{
[super loadView];
UIButton *backButton = [[UIButton alloc] initWithFrame: CGRectMake(0, 0, 44.0f, 30.0f)];
[backButton setImage:[UIImage imageNamed:#"back.png"] forState:UIControlStateNormal];
[backButton addTarget:self action:#selector(popVC) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
}
And off course:
- (void) popVC{
[self.navigationController popViewControllerAnimated:YES];
}
Target:
customizing all back button on UINavigationBar to an white icon
Steps:
1. in "didFinishLaunchingWithOptions" method of AppDelete:
UIImage *backBtnIcon = [UIImage imageNamed:#"navBackBtn"];
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")) {
[UINavigationBar appearance].tintColor = [UIColor whiteColor];
[UINavigationBar appearance].backIndicatorImage = backBtnIcon;
[UINavigationBar appearance].backIndicatorTransitionMaskImage = backBtnIcon;
}else{
UIImage *backButtonImage = [backBtnIcon resizableImageWithCapInsets:UIEdgeInsetsMake(0, backBtnIcon.size.width - 1, 0, 0)];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:backButtonImage forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -backButtonImage.size.height*2) forBarMetrics:UIBarMetricsDefault];
}
2.in the viewDidLoad method of the common super ViewController class:
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")) {
UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithTitle:#""
style:UIBarButtonItemStylePlain
target:nil
action:nil];
[self.navigationItem setBackBarButtonItem:backItem];
}else{
//do nothing
}
SWIFT 4
For those looking to create custom back buttons as well as have their title removed please use the following piece of code within the view controller that's pushing the new one:
self.navigationController?.navigationBar.backIndicatorImage = UIImage(named: "close")
self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "close")
self.navigationItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
For a more universal use, do the following:
Create a universal function as follows:
func addCustomizedBackBtn(navigationController: UINavigationController?, navigationItem: UINavigationItem?) {
navigationController?.navigationBar.backIndicatorImage = UIImage(named: "close")
navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "close")
navigationItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
Then use it in the view controllers as follows:
addCustomizedBackBtn(navigationController: self.navigationController, navigationItem: self.navigationItem)
Simple hack from iOS6 works on iOS7 too:
[UIBarButtonItem.appearance setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];
Edit:
Don't use this hack. See comment for details.
you can use this. This works perfectly for me by just adding a UIButton as a custumview for the UIBarButtonItem.
Try the Below Code
self.navigationItem.leftBarButtonItem=[self backButton];
- (UIBarButtonItem *)backButton
{
UIImage *image = [UIImage imageNamed:#"back-btn.png"];
CGRect buttonFrame = CGRectMake(0, 0, image.size.width, image.size.height);
UIButton *button = [[UIButton alloc] initWithFrame:buttonFrame];
[button addTarget:self action:#selector(backButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[button setImage:image forState:UIControlStateNormal];
UIBarButtonItem *item= [[UIBarButtonItem alloc] initWithCustomView:button];
return item;
}
Create a UILabel with the title you want for your root view controller and assign it to the view controller's navigationItem.titleView.
Now set the title to an empty string and the next view controller you push will have a back button without text.
self.navigationItem.titleView = titleLabel; //Assuming you've created titleLabel above
self.title = #"";
// add left bar button item
try this code:
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage* image_back = [UIImage imageNamed:#"your_leftarrowImage.png"];
CGRect backframe = CGRectMake(250, 9, 15,21);
UIButton *backbutton = [[UIButton alloc] initWithFrame:backframe];
[backbutton setBackgroundImage:image_back forState:UIControlStateNormal];
[backbutton addTarget:self action:#selector(Btn_back:)
forControlEvents:UIControlEventTouchUpInside];
[backbutton setShowsTouchWhenHighlighted:YES];
UIBarButtonItem *backbarbutton =[[UIBarButtonItem alloc] initWithCustomView:backbutton];
self.navigationItem.leftBarButtonItem=backbarbutton;
[backbutton release];
}
-(IBAction)Btn_back:(id)sender
{
[self.navigationController popViewControllerAnimated:YES];
}
All the answers do not solve the issue. It is not acceptable to set back button title in every view controller and adding offset to the title still makes next View Controller title shift to the right.
Here is the method using method swizzling, just create new extension to UINavigationItem
import UIKit
extension UINavigationItem {
public override class func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}
// make sure this isn't a subclass
if self !== UINavigationItem.self {
return
}
dispatch_once(&Static.token) {
let originalSelector = Selector("backBarButtonItem")
let swizzledSelector = #selector(UINavigationItem.noTitleBackBarButtonItem)
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
// MARK: - Method Swizzling
struct AssociatedKeys {
static var ArrowBackButtonKey = "noTitleArrowBackButtonKey"
}
func noTitleBackBarButtonItem() -> UIBarButtonItem? {
if let item = self.noTitleBackBarButtonItem() {
return item
}
if let item = objc_getAssociatedObject(self, &AssociatedKeys.ArrowBackButtonKey) as? UIBarButtonItem {
return item
} else {
let newItem = UIBarButtonItem(title: " ", style: UIBarButtonItemStyle.Plain, target: nil, action: nil)
objc_setAssociatedObject(self, &AssociatedKeys.ArrowBackButtonKey, newItem as UIBarButtonItem?, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newItem
}
}
}
I applied the following code in viewDidLoad and it works:
// this will set the back button title
self.navigationController.navigationBar.topItem.title = #"Test";
// this line set the back button and default icon color
//[[self.navigationController.navigationBar.subviews lastObject] setTintColor:[UIColor blackColor]];
this line change the back default icon to your custom icon
[[self.navigationController.navigationBar.subviews lastObject] setTintColor:[UIColor colorWithPatternImage:[UIImage imageNamed:#"menuicon"]]];
Just to update I use Vector Icon
You can subclass UINavigationController, set itself as the delegate, and set the backBarButtonItem in the delegate method navigationController:willShowViewController:animated:
#interface Custom_NavigationController : UINavigationController <UINavigationControllerDelegate>
#end
#implementation Custom_NavigationController
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
}
#pragma mark - UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
viewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"" style:UIBarButtonItemStylePlain target:nil action:nil];
}
#end
Set back title empty
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:#"" style:UIBarButtonItemStyleDone target:self action:#selector(handleBack:)];
[backButton setTintColor:Color_WHITE];
[self.navigationItem setBackBarButtonItem:backButton];
Change back image
UIImage *backImg = [[UIImage imageNamed:#"ic_back_white"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
[UINavigationBar appearance].backIndicatorImage = backImg;
[UINavigationBar appearance].backIndicatorTransitionMaskImage = backImg;
Check this answer
How to change the UINavigationController back button name?
set title text to string with one blank space as below
title = " "
Don't have enough reputation to add comments :)
I have been using this solution since iOS 5 or so without any problems. I made a utility function that I call in my view controllers. You need to do it either in viewDidLoad or any point after that.
void updateBackButtonTextForViewController(UIViewController *viewController, NSString *text)
{
if(! viewController.navigationItem.backBarButtonItem)
{
viewController.navigationItem.backBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:text
style:UIBarButtonItemStylePlain
target:nil action:nil];
}
else
{
viewController.navigationItem.backBarButtonItem.title = text;
}
}
In some cases the navigation item may already exist, in other cases it needs to be created. This accounts for both of those cases without messing with the navigation item title. It allows you to remove the title by simply passing in #"".
The only way that worked for me was:
navigationController?.navigationBar.backItem?.title = ""
UPDATE:
When I changed the segue animation flag to true (It was false before), the only way that worked for me was:
navigationController?.navigationBar.topItem?.title = ""
If you have two ViewController(FirstVC, SecondVC) Embed in Navigation Controller, and you want there is only back arrow in SecondVC.
You can try this
In FirstVC's ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .Plain, target: nil, action: nil)
}
Then when you push into SecondVC, you'll see the there is only back arrow
If you set the tintColor Of NavigationBar,add a custom back button image without title that tint color will reflect the image color. Please follow this apple documentaion link.
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/index.html#//apple_ref/doc/uid/TP40012857-UIView-SW7
UINavigationItem *navItem = [[UINavigationItem alloc] init];
navBar.tintColor = self.tintColor;
UIImage *myImage = [UIImage imageNamed:#"left_arrow.png"];
myImage = [myImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIBarButtonItem *leftButton = [[UIBarButtonItem alloc] initWithImage:myImage style:UIBarButtonItemStylePlain target:self action:#selector(cancelButtonFunction:)];
navItem.leftBarButtonItem = leftButton;
navBar.items = #[ navItem ];
I'm written an extension to make this easier:
extension UIViewController {
/// Convenience for setting the back button, which will be used on any view controller that this one pushes onto the stack
#objc var backButtonTitle: String? {
get {
return navigationItem.backBarButtonItem?.title
}
set {
if let existingBackBarButtonItem = navigationItem.backBarButtonItem {
existingBackBarButtonItem.title = newValue
}
else {
let newNavigationItem = UIBarButtonItem(title: newValue, style:.plain, target: nil, action: nil)
navigationItem.backBarButtonItem = newNavigationItem
}
}
}
}
You can change the title to #"" of the current ViewController on viewWillDisappear, and when it's about to show again re-set the title to whatever it was before.
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.title = #"Previous Title";
}
-(void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.title = #"";
}
Change backItem.title = "" to using topItem.title = ""
Setting navigationItem.hidesBackButton = true & navigationItem.leftBarButtonItem will lose the back gesture
Remember we have to create 2 instances of the back image
My solution will change the image & keep the back gesture:
navigationController?.navigationBar.backIndicatorImage = UIImage(named: "back")
navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "back")
navigationController?.navigationBar.topItem?.title = ""
In the prepareForSegue: method of your first ViewController you set that views title to #"", so when the next view is pushed it will display the previous ViewController title which will be #"".
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
self.navigationItem.title = #" ";
}
The only problem with this is that when you hit the back button your previous view won't have a title, so you may add it again on viewWillAppear:
-(void)viewWillAppear:(BOOL)animated{
self.navigationItem.title = #"First View Title";
}
I don't like very much this solution but it works and i didn't find other way to do it.
To add to Thomas C's answer above, sometimes putting a single space doesn't work and you have to keep adding spaces.
You'll know you succeeded when you see "Bar Button Item - " under the "Navigation Item". That's in the Document Outline (Editor->Show Document Outline). Once you see the above picture, you can delete a few spaces and see if it still works.

Making a button persistent across all view controllers

I want to have a persistent button in the bottom right corner of my app. During all view transitions, the button should remain static. I'm having trouble deciding what view to add the button to. I know the button ought to be stored in the AppDelegate, but I don't know what other view it would be sense to add it to except the window. One downside of adding it to the window is that when there's an app running in the background (ie Phone), the added status bar padding will push down the window. In general, adding it to the window seems to be a hacky solution -- any thoughts?
Yes, adding it to the UIWindow would be extremely hacky and finicky.
Storyboards
If you're using Storyboards and iOS 5.0 onwards, you should be able to use container views and do something like this:
Here's another picture showing the, rather simplistic, structure of the first View Controller:
The view controller on the left has a container, and then a view which holds the button on top of it. The container indicates that the navigation controller (directly to the right) should appear within itself, that relationship is shown by the =([])=> arrow (formally known as an embed segue). Finally the navigation controller defines its root view controller to the one on the right.
In summary, the first view controller pancakes-in the container view with the button on top, so everything that happens inside has to have the button on top.
Using childViewControllers
aka. The "I hate Storyboards and puppies" mode
Using a similar structure to the Storyboard version, you could create the base view controller with its button, and then, add the view that will become then new "root" of the application, underneath.
To make it clear, let's call the one view controller that holds the button the FakeRootViewController, and the view controller that will be, for all practical purposes, the root of the application: RootViewController. All subsequent view controllers won't even know that there's the FakeRootViewController above everyone else.
FakeRootViewController.m
// The "real" root
#import "RootViewController.h"
// Call once after the view has been set up (either through nib or coded).
- (void)setupRootViewController
{
// Instantiate what will become the new root
RootViewController *root = [[RootViewController alloc] <#initWith...#>];
// Create the Navigation Controller
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:root];
// Add its view beneath all ours (including the button we made)
[self addChildViewController:nav];
[self.view insertSubview:nav.view atIndex:0];
[nav didMoveToParentViewController:self];
}
AppDelegate.m
#import "FakeRootViewController.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
FakeRootViewController *fakeRoot = [[FakeRootViewController alloc] <#initWith...#>];
self.window.rootViewController = fakeRoot;
[self.window makeKeyAndVisible];
return YES;
}
That way, you can have all the benefits of inserting the button on the window, without all the guilt and "Should I really be a programmer?" that it causes.
Potentially you could have 1 main "root" view controller, and all you other view controllers could be child view controllers, with their views as child views. Then they would have their content, and the button would be in the "root" view controller. But this seems just as sketchy and hacky as putting it in the window, and probably less convenient.
I use this button:
#interface UIPopUpButton : UIImageView <UIPopoverControllerDelegate, UIActionSheetDelegate>
{
UIPopoverController* popoverController;
Class popoverClass;
}
- (id) initWithPoint: (CGPoint) point;
- (void) touchesBegan: (NSSet*) touches
withEvent: (UIEvent*) event;
+ (id) buttonAtPoint: (CGPoint) point;
+ (id) buttonAtOriginalPoint;
+ (void) unhighlight;
+ (void) bringButtonToFront;
#property (nonatomic, retain) UIPopoverController* popoverController;
#property (nonatomic, assign) Class popoverClass;
#end
#import "UIPopUpButton.h"
#implementation UIPopUpButton
static UIPopUpButton* button = nil;
static CGPoint originalPoint;
#synthesize popoverClass;
#synthesize popoverController;
+ (id) buttonAtPoint: (CGPoint) point
{
if (button == nil)
{
button = [[UIPopUpButton alloc] initWithPoint: point];
originalPoint = point;
button.popoverClass = [UIPopoverController class];
}
else
{
button.frame = CGRectMake(point.x, point.y, button.frame.size.width, button.frame.size.height);
}
return button;
}
+ (id) buttonAtOriginalPoint
{
return [self buttonAtPoint: originalPoint];
}
+ (void) unhighlight
{
button.highlighted = NO;
}
+ (void) bringButtonToFront
{
[[UIApplication sharedApplication].keyWindow addSubview: [self buttonAtOriginalPoint]];
}
- (id) initWithPoint: (CGPoint) point
{
UIImage* image1 = [UIImage imageNamed: #"topbutton.png"];
UIImage* image2 = [UIImage imageNamed: #"topbutton.png"];
if ((self = [super initWithImage: image1
highlightedImage: image2]))
{
self.userInteractionEnabled = YES;
self.frame = CGRectMake(point.x, point.y, self.frame.size.width, self.frame.size.height);
self.multipleTouchEnabled = NO;
}
return self;
}
- (BOOL) isAppCurrStatus
{
return ([DevToolsClientController sharedInstance].statusOfRootViewController == FrontEndApplication);
}
- (void) touchesBegan: (NSSet*) touches withEvent: (UIEvent*) event
{
UITouch* touch = [touches anyObject];
if(touch.view == self)
{
if (self.popoverController == nil)
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
UIActionSheet* actionSheet = [[UIActionSheet alloc] initWithTitle: #"Please choice operation:"
delegate: self
cancelButtonTitle: nil
destructiveButtonTitle: nil
otherButtonTitles: nil];
[actionSheet addButtonWithTitle: #"Cancel"];
actionSheet.cancelButtonIndex = 0;
[actionSheet addButtonWithTitle: #"Button 1"];
actionSheet.actionSheetStyle = UIActionSheetStyleDefault;
[actionSheet setTag: 0];
[actionSheet setDelegate: self];
[actionSheet showInView: [self superview]];
[actionSheet release];
[actions release];
}
else
{
PopoverMenuController* contentViewController = [[PopoverMenuController alloc] init];
self.popoverController = [[UIPopoverController alloc] initWithContentViewController: contentViewController];
popoverController.delegate = self;
[popoverController presentPopoverFromRect: CGRectMake(10.0f, 10.0f, 5.0f, 5.0f)
inView: self
permittedArrowDirections: UIPopoverArrowDirectionAny
animated: YES];
contentViewController.popoverController = self.popoverController;
[contentViewController reloadData];
}
}
else
{
[self.popoverController dismissPopoverAnimated:YES];
self.popoverController = nil;
}
}
[super touchesBegan: touches withEvent: event];
}
#pragma mark UIActionSheetDelegate implementation
-(void) actionSheet: (UIActionSheet*) actionSheet clickedButtonAtIndex: (NSInteger) buttonIndex
{
NSNumber* indexAction = [[NSNumber alloc] initWithInt: buttonIndex - 1];
}
- (void) runAction: (NSNumber*) indexAction
{
[DevToolsPopoverMenuController runAction: [indexAction integerValue]];
}
#pragma mark -
#pragma mark UIPopoverControllerDelegate implementation
- (void) popoverControllerDidDismissPopover: (UIPopoverController*) thePopoverController
{
if (self.popoverController != nil)
{
self.popoverController = nil;
}
}
- (BOOL) popoverControllerShouldDismissPopover: (UIPopoverController*) thePopoverController
{
//The popover is automatically dismissed if you click outside it, unless you return NO here
return YES;
}
#end
call:
[UIPopUpButton bringButtonToFront];
My button is always on top.
Try subclassing the UIViewController class and make your own one with the button
Create a singleton object that holds the button so all view controllers can reference it and add it to their subview or add it to the window directly.
SomeClass.h
#property (nonatomic) UIButton *yourButton;
+(SomeClass*)sharedSomeClass;
SomeClass.m
#synthesize yourButton = _yourButton;
-(id)init
{
self = [super init];
if(self)
{
_yourButton = [UIButton new];
//Other settings you want for your button
}
return self;
}
+(SomeClass)sharedSomeClass
{
static SomeClass *sharedSomeClass;
if (!sharedSomeClass)
sharedSomeClass = [[super allocWithZone:nil]init];
return sharedSomeClass;
}
+(void)allocWithZone:(NSZone*)zone
{
return [self sharedSomeClass];
}
If you like you can access the window directly like this:
UIWindow *mainwindow = [[[UIApplication sharedApplication]delegate]window];
import SomeClass.h into your view controllers, and access the button from anywhere
#import "SomeClass.h"
SomeClass *someClass = [SomeClass sharedSomeclass];
UIButton *localButton = someClass.yourButton;

Resources