13

I am trying to set the background image for back button in normal and highlighted states.

- (void)configureBackButtonInNavigationItem:(UINavigationItem *)item
{
    UIBarButtonItem *backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"back"
            style:UIBarButtonItemStyleBordered target:nil action:NULL];
    [backBarButtonItem setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor whiteColor]} forState:UIControlStateNormal];
    [backBarButtonItem setTitleTextAttributes:@{NSForegroundColorAttributeName : [UIColor orangeColor]} forState:UIControlStateHighlighted];
    
    // white arrow image
    UIImage *normalImage = [[[UIImage imageNamed:@"btn_normal"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, 17.f, 0.f, 0.f)];

    // orange arrow image
    UIImage *pressedImage = [[[UIImage imageNamed:@"btn_on_press"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, 17.f, 0.f, 0.f)];
    
    [backBarButtonItem setBackButtonBackgroundImage:normalImage
                    forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [backBarButtonItem setBackButtonBackgroundImage:pressedImage
                forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
                
    [backBarButtonItem setBackgroundImage:normalImage
                    forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [backBarButtonItem setBackgroundImage:pressedImage
                forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
                
    NSLog(@"NORMAL: %@ HIGHLIGHTED: %@", [backBarButtonItem backButtonBackgroundImageForState:UIControlStateNormal barMetrics:UIBarMetricsDefault],
                [backBarButtonItem backButtonBackgroundImageForState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault]);
    item.backBarButtonItem = backBarButtonItem;
    
    NSLog(@"NORMAL: %@ HIGHLIGHTED: %@", [backBarButtonItem backButtonBackgroundImageForState:UIControlStateNormal barMetrics:UIBarMetricsDefault],
                [backBarButtonItem backButtonBackgroundImageForState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault]);
}

The output is following:

NORMAL: <_UIResizableImage: 0x16b55e10> HIGHLIGHTED: <_UIResizableImage: 0x16b593d0>
NORMAL: <_UIResizableImage: 0x16b55e10> HIGHLIGHTED: <_UIResizableImage: 0x16b593d0>

But observed result for highlighted state is just dimming of what was set to the normal state instead of using the correct highlighted image.

Normal:

Normal state of back bar button item

Highlighted (Arrow is still white, button is dimmed unexpectedly):

Highlighted state of back bar button item

Please do not post answers regarding usage of leftBarButtonItem or UIButton as custom view. Both these approaches break swipe-to-go-back behaviour available on iOS 7.

UPD: filled radar #17481106 regarding this issue.

UPD2: radar #17481106 fixed in iOS 8.

Eugene Dudnyk
  • 5,553
  • 1
  • 23
  • 48
  • Desperately trying to find a solution that doesn't effect the alpha of the back button but it looks like its now the iOS7 default effect. Anywho, have you seen the properties `setBackIndicatorImage` and `setBackIndicatorTransitionMaskImage`? The work really well for keeping the new iOS7 style back transition. (They do however change the opacity on highlight, sorry). W – William George May 15 '14 at 00:18
  • Noticed that if I use backIndicatorImage, it is not centered vertically correctly in NavigationBar if it has bigger height than system one. Ugly appearance. – Eugene Dudnyk May 15 '14 at 14:07
  • you can get swipe behavior by posting an action on custom button with following code [self.navigationController popViewControllerAnimated:YES]; – Vikas Jun 24 '14 at 10:10
  • system swipe-to-go-back works not only on navigation button, but also on the left side of whole screen area. Not sure that it's a good approach to work-around that also. Looks like fighting the framework. – Eugene Dudnyk Jun 24 '14 at 14:52
  • Have you tried this using appearance? [[UIBarButtonItem appearance] setBackButtonBackgroundImage:backButtonImage forState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; Same for highlighted state. – Alexander Jun 24 '14 at 15:04
  • The result is the same as described in the question. – Eugene Dudnyk Jun 24 '14 at 15:22

3 Answers3

5

Currently Apple has bug on interactivePopGestureRecognizer (which makes to freeze navigation controller's view after swiping back on push animation, you will see nested pop animation can result in corrupted navigation bar warning in console), by the way, we can make a small hack to work around that bug.

Here is a solution that works fine for me,

Subclass a NavigationController class and make it to delegate the gesture

@interface CBNavigationController : UINavigationController 
@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;
}


@end

When the user starts swiping backwards in the middle of a transition, the pop events stack up and "corrupt" the navigation stack. My workaround is to temporarily disable the gesture recognizer during push transitions, and enable it again when the new view controller loads. Again, this is easier with a UINavigationController subclass.

After this, you can calmly use item.leftBarButtonItem and UIButton as custom view.

l0gg3r
  • 8,864
  • 3
  • 26
  • 46
3

In addition to l0gg3r's answer, you can make a subclass of UINavigationBar where you can implement l0gg3r's logic and customize your back button.
After which you just have to set the class name to your navigationBar from storyboard.

Something like this:

#import "MyNavigationBar.h"

#import <objc/runtime.h>
#import <objc/message.h>    

#pragma mark - UINavigationController category

@interface UINavigationController (InteractiveGesture) <UINavigationControllerDelegate, UIGestureRecognizerDelegate>

- (void)fixInteractivePopGesture;

@end

@implementation UINavigationController (InteractiveGesture)

- (void)fixInteractivePopGesture
{
    __weak UINavigationController *weakSelf = self;

    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.interactivePopGestureRecognizer.delegate = weakSelf;
        self.delegate = weakSelf;
    }

    [self swizzleOriginalSelectorWithName:@"pushViewController:animated:" 
                       toSelectorWithName:@"myPushViewController:animated:"];
}

#pragma mark - Swizzle method
- (void)swizzleOriginalSelectorWithName:(NSString *)origName toSelectorWithName:(NSString *)swizzleName
{
    Method origMethod = class_getInstanceMethod([self class], NSSelectorFromString(origName));
    Method newMethod = class_getInstanceMethod([self class], NSSelectorFromString(swizzleName));
    method_exchangeImplementations(origMethod, newMethod);
}

- (void)myPushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.interactivePopGestureRecognizer.enabled = NO;
    }

    [self myPushViewController: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;
    }
}

@end

#pragma mark - MyNavigationBar

@interface MyNavigationBar()

@property (strong, nonatomic) UIButton *backButtonCustomView;

@end

@implementation MyNavigationBar

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];

    [self setup];
}

- (void)setup
{
    self.backButtonCustomView = [UIButton buttonWithType:UIButtonTypeCustom];
    // here customize your button        
    // e.g. set images for Normal state, or highlighted state, etc...
    // ...
    [self.backButtonCustomView addTarget:self action:@selector(handleBackButton:) forControlEvents:UIControlEventTouchUpInside];

    self.backButton = [[UIBarButtonItem alloc] initWithCustomView: self.backButtonCustomView];        
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    if ([[self navigationController] viewControllers].count > 1) {
        [self.topItem setLeftBarButtonItem:self.backButton animated:YES];
    }

    // Enabling back "Swipe from edge to pop" feature.
    [self.navigationController fixInteractivePopGesture];
}

- (void)handleBackButton:(id)sender
{
    UINavigationController *nvc = [self navigationController];

    [nvc popViewControllerAnimated:YES];
}

- (UINavigationController *)navigationController
{
    UINavigationController *resultNC = nil;
    UIViewController *vc = nil;
    for (UIView* next = [self superview]; next; next = next.superview) {
        UIResponder* nextResponder = [next nextResponder];

        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            vc = (UIViewController*)nextResponder;
            break;
        }
    }

    if (vc) {
        if ([vc isKindOfClass:[UINavigationController class]]) {
            resultNC = (UINavigationController *)vc;
        } else {
            resultNC = vc.navigationController;
        }
    }

    return resultNVC;
}

@end

Then:
enter image description here

Here you go. Thats it! Now you can just copy/paste that class into any project you want and just set class name from storyboard :)

arturdev
  • 10,884
  • 2
  • 39
  • 67
-3

You didn't want a custom view because it would break the swiping, but you should add this line.

self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self; 

Your code would be something like below.

UIImage *normalImage = [[[UIImage imageNamed:@"btn_normal"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, 17.f, 0.f, 0.f)];
UIImage *pressedImage = [[[UIImage imageNamed:@"btn_on_press"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] resizableImageWithCapInsets:UIEdgeInsetsMake(0.f, 17.f, 0.f, 0.f)];    

UIButton *customBackButton = [UIButton buttonWithType:UIButtonTypeCustom];
[customBackButton setBackgroundImage:normalImage forState:UIControlStateNormal];
[customBackButton setBackgroundImage:pressedImage forState:UIControlStateHighlighted];
[customBackButton addTarget:self action:@selector(customBackMethod:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *customBackBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:customBackButton];
self.navigationItem.leftBarButtonItem = customBackBarButtonItem;
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;

- (IBAction)customBackMethod:(id)sender {
    [self.navigationController popViewControllerAnimated:YES];
}
yoeriboven
  • 3,541
  • 3
  • 25
  • 40