82

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.

2
  • I am yet to find a proper solution for this?? IS there anyone who has found a good solution and explain why that is working??. Nov 26, 2013 at 8:10
  • How about using well-made third party library: SwipeBack?
    – devxoul
    Feb 27, 2015 at 10:27

14 Answers 14

83

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.

18
  • Do you set this on the view controller? or the navigation controller? I have the same problem but this doesn't seem to work? Sep 30, 2013 at 16:19
  • 1
    @mixedCase After downloading your sample project I now understand your issue. The code works as long as the collection view's content does not exceed the horizontal width of the view controller. However, as soon as the collection view becomes horizontally scrollable it overrides the interactivePopGestureRecognizer. I will see if I can find a work-around. Oct 2, 2013 at 7:22
  • 8
    I have issue with this code. It works not a long time, after 5-10 back-swipes VC freezes. Any solution? Nov 19, 2013 at 15:38
  • 1
    Timur Bernikowich I experiencing the same.Did you find any reasons for this? Nov 19, 2013 at 20:07
  • 4
    Setting the delegate to self unsets it from an object of class _UINavigationInteractiveTransition. It is the responsibility of that object to ensure that the navigation controller is not told to pop while it is already in transition. Still investigating whether its possible to enable this gesture or not when the back button is custom.
    – Saltymule
    Dec 2, 2013 at 13:51
55

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.

7
  • 4
    Thanks Dan, this is really important and should be the accepted answer. By simply re-assigning the delegate you open yourself to A LOT of weird behavior. Particularly when users try to swipe back on the topViewController. Jan 22, 2014 at 23:00
  • 1
    This is a solution assuming that all you want to do is change the back button image. But what if you want to change the back button text and/or the action performed when clicking the back button?
    – user102008
    Mar 12, 2014 at 0:04
  • At that point you would be better served by setting the UINavigationItem.leftBarButtonItem. There are a variety of answers that can be found by searching leftBarButtonItem on google or stackoverflow.
    – Saltymule
    Mar 12, 2014 at 13:48
  • If you want to limit the appearance change to your own UI and not affect navigation bars that are for example created by ABPeoplePickerNavigationController you can use a custom UINavigationController subclass: [[UINavigationBar appearanceWhenContainedIn:[THNavigationController class], nil] setBackIndicatorImage:[UIImage imageNamed:@"btn_back_arrow"]]; [[UINavigationBar appearanceWhenContainedIn:[THNavigationController class], nil] setBackIndicatorTransitionMaskImage:[UIImage imageNamed:@"btn_back_arrow_highlighted"]];
    – tom
    Mar 26, 2014 at 9:46
  • 2
    Unfortunately, this doesn't work on iOS8. The back button doesn't change its appearance to my custom image.
    – Lensflare
    Oct 27, 2014 at 12:54
29

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
2
  • 1
    had the same problems with swiping on the root view controller, thanks!
    – rosem
    Apr 11, 2014 at 19:24
  • THE BEST SOLUTION IN THE WHOLE PAGE. WORKS PERFECTLY WELL. THANKS Feb 1, 2015 at 23:17
19

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];
4
  • 2
    I have seen comments elsewhere that this causes problems in 64 bit mode due to a bug in Apple's code at present- just thought I'd post a warning Nov 13, 2013 at 15:43
  • @PeterJohnson what kind of bug?
    – art-divin
    Feb 13, 2014 at 15:55
  • Simply the best solution!
    – Igotit
    Jul 28, 2014 at 6:44
  • Simplest solution ever.
    – tounaobun
    Jul 24, 2015 at 10:27
8

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 {}
0
6

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;
}
4
  • 4
    One problem with this method is if you perform an edge pan on a root view it will lock up the UI. I encountered this because I'm pushing multiple instances of the same view to the nav stack. Oct 14, 2013 at 10:57
  • @MrNickBarker Thanks for letting me know! Could you please describe the exact scenario? I couldn't reproduce it when swiping the root view controller Oct 14, 2013 at 14:45
  • 4
    I was setting it in the viewDidLoad method. My final solution was to set another class to be the delegate which just returns true for 'gestureRecognizerShouldBegin' if there is more than one view controller in the nav stack. Oct 14, 2013 at 16:52
  • MrNickBarker any reasons why it freezes I am experiencing the same Nov 19, 2013 at 20:09
6
navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;

This is from http://stuartkhall.com/posts/ios-7-development-tips-tricks-hacks, but it causes several bugs:

  1. Push another viewController into the navigationController when swiping in from the left edge of the screen;
  2. 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;
}
1
  • 1
    SwipeBack is a solution for these problems.
    – devxoul
    Feb 27, 2015 at 10:29
4

Try self.navigationController.interactivePopGestureRecognizer.enabled = YES;

2
  • No, it's already enabled.. the problem seems to be that its delegate doesn't allow the gesture-recognizer to start, i've added my solution as another answer here. Oct 13, 2013 at 8:39
  • avishic could you please cater to explain why setting the delegate will work?..I am stuck on the same problem. Nov 21, 2013 at 21:14
1

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;
}
3
  • UI freezes if one swipes left on the root view controller of the Navigation controller. Jul 29, 2015 at 8:59
  • @JohnVanDijk I edited the answer with a fix that I think I implemented to solve that problem. Has been a while, but it makes sense. Basically, if the top view controller is the root view controller, then we will not respond to the 'interactivePopGestureRecognizer'
    – kgaidis
    Jul 29, 2015 at 14:23
  • It is hiding my back button Sep 13, 2016 at 11:57
1

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 {}
1

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;
        }
    }
}
0

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.

0

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.

0

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
}}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.