70

How can I change the position of a UIBarButtonItem in a UINavigationBar? I would like my button to be about 5px higher than its normal position.

AstroCB
  • 12,337
  • 20
  • 57
  • 73
Ben Williams
  • 4,695
  • 9
  • 47
  • 72
  • Do you also want the UINavigation bar to be behind it or just have the button free floating? – Jab Apr 23 '11 at 04:07
  • Not quite sure what you mean - I think the answer is that I want it to behave exactly as a normal UIBarButtonItem inside a UINavigationBar would. Say, if it was the leftBarButtonItem...everything the same, just 5px higher. The button is using a custom image, as well, if that's relevant. – Ben Williams Apr 24 '11 at 10:44
  • one approach is, simply use images instead. this "always works" even though it's a hassle. – Fattie Nov 10 '13 at 16:20

21 Answers21

133

This code creates a back button for UINavigationBar with image background and custom position. The trick is to create an intermediate view and modify its bounds.

Swift 5

let menuBtn = UIButton(type: .custom)
let backBtnImage = UIImage(named: "menu")

menuBtn.setBackgroundImage(backBtnImage, for: .normal)
menuBtn.addTarget(self, action: #selector(showMenuTapped), for: .touchUpInside)
menuBtn.frame = CGRect(x: 0, y: 0, width: 45, height: 45)

let view = UIView(frame: CGRect(x: 0, y: 0, width: 45, height: 45))
view.bounds = view.bounds.offsetBy(dx: 10, dy: 3)
view.addSubview(menuBtn)
let backButton = UIBarButtonItem(customView: view)

navigationItem.leftBarButtonItem = backButton

Objective C

UIButton *backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *backBtnImage = [UIImage imageNamed:@"btn-back"];
UIImage *backBtnImagePressed = [UIImage imageNamed:@"btn-back-pressed"];
[backBtn setBackgroundImage:backBtnImage forState:UIControlStateNormal];
[backBtn setBackgroundImage:backBtnImagePressed forState:UIControlStateHighlighted];
[backBtn addTarget:self action:@selector(goBack) forControlEvents:UIControlEventTouchUpInside];
backBtn.frame = CGRectMake(0, 0, 63, 33);
UIView *backButtonView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 63, 33)];
backButtonView.bounds = CGRectOffset(backButtonView.bounds, -14, -7);
[backButtonView addSubview:backBtn];
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithCustomView:backButtonView];
self.navigationItem.leftBarButtonItem = backButton;
SwiftiSwift
  • 7,528
  • 9
  • 56
  • 96
Evgeny Shadchnev
  • 7,320
  • 4
  • 27
  • 30
  • unfortunately does not work when the UIBarButtonItem is inside a UIToolbar. :( – Duck Apr 25 '13 at 16:39
  • 32
    A problem with this however is that the area that can be tapped is still restricted to the default UIBarButtonItem area. Moving the UIButton will probably cause the area that can be tapped to get smaller and thus sometime make it seem as if the UIButton doesn't recognise the tap. – iMaddin Sep 11 '13 at 14:20
  • I agree with @iMaddin, the touch area becomes smaller and smaller the larger your offset gets. – ajmccall Nov 03 '13 at 17:10
  • 3
    Setting offset (by `CGRectOffset`) for UIButton doesn't work in iOS7. UIBarButtonItem seems to have fixed position in iOS7 and moving/offsetting x,y coordinate of UIButton doesn't work. Modifying width and height of UIButton works but no impact of changing x,y coordinates. – Saurabh Hooda Nov 23 '13 at 07:30
  • I don't see how this answer gives a solution to changing the position of the `UIBarButtonItem`. It only ads complexity and causes other issues like tapping area. I think this matter should be opened up as a feature or bug on apple's issue reporting pages, since a lot of developers would like to change this coordinates or have a bit more control over the button – Roland Dec 21 '13 at 18:25
  • Best workaround for the problem in tap target size shrinking with this answer is here: http://stackoverflow.com/a/18918378/115676 – AlexD Jan 07 '14 at 19:11
  • I found that adding a TapGestureRecognizer to the UIBarButtonItem customView took care of the shrinking hit area when offset. – Nate Potter Mar 12 '14 at 00:33
  • @iMaddin - this can be fixed by subclassing the wrapper view (superview of button, custom view of uibarbuttonitem) and overriding the hittest method to return button accordingly. – JakubKnejzlik Jul 21 '14 at 19:43
  • What is the code for this for using text instead of images? – Timothy Swan Aug 11 '14 at 02:58
  • You can always add a tap gesture recognizer to the intermediate view to receive the tap action. – strongwillow Jul 22 '16 at 07:17
  • @JakubKnejzlik do you have an example please? – NiBE Oct 04 '16 at 09:57
  • This answer worked for me in Obj-C but it definitely reduced the tap target size. Does anyone have a more recent solution for this? iOS 15+... I see the comment now by @NatePotter . I will try that and report back. – Jamie M. Nov 07 '22 at 14:14
42

I solved using transform and custom view:

(Swift)

  // create the button
  let suggestImage  = UIImage(named: "tab-item-popcorn-on")!.imageWithRenderingMode(.AlwaysOriginal)
  let suggestButton = UIButton(frame: CGRectMake(0, 0, 40, 40))
  suggestButton.setBackgroundImage(suggestImage, forState: .Normal)
  suggestButton.addTarget(self, action: Selector("suggesMovie:"), forControlEvents:.TouchUpInside)

  // here where the magic happens, you can shift it where you like
  suggestButton.transform = CGAffineTransformMakeTranslation(10, 0)

  // add the button to a container, otherwise the transform will be ignored  
  let suggestButtonContainer = UIView(frame: suggestButton.frame)
  suggestButtonContainer.addSubview(suggestButton)
  let suggestButtonItem = UIBarButtonItem(customView: suggestButtonContainer)

  // add button shift to the side
  navigationItem.rightBarButtonItem = suggestButtonItem
Adriano Spadoni
  • 4,540
  • 1
  • 25
  • 25
35

There is no particularly good way to do this. Your best bet if you really must is to subclass UINavigationBar, and override layoutSubviews to call [super layoutSubviews] and then find and reposition the button's view.

Anomie
  • 92,546
  • 13
  • 126
  • 145
  • I gave this a try, and it works, but you can see the button jumping around. So when the view is transitioning it's in the default position, then when the view is in place the button jumps. – Ben Williams Apr 29 '11 at 00:29
  • 2
    Actually, I take it back. It works nicely. The jumping was a mistake on my behalf. Thanks! – Ben Williams Apr 29 '11 at 01:00
  • 8
    How did you get it to stop jumping? I'm having the same issue – Adam Aug 17 '11 at 13:58
  • 3
    Thank you for sharing this. Could you provide a sample? Thank you in advance. – Lorenzo B Oct 02 '11 at 11:00
  • Adjusting the frames of the leftBarButtonItem.customView and rightBarButtonItem.customView and not the center property prevented the jumping for me. – Nate Potter Oct 12 '12 at 14:55
  • This is a good sample! Look at the second answer it is more safe: http://stackoverflow.com/a/17434530/1351190 – Simone Lai Oct 18 '13 at 14:15
  • 14
    Does no one know about using the appearance method? Put this in your app delegate: `[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil] setBackButtonBackgroundVerticalPositionAdjustment:-3 forBarMetrics:UIBarMetricsDefault]; ` – barndog Dec 30 '13 at 08:10
  • @Adam see http://stackoverflow.com/questions/5761183/change-position-of-uibarbuttonitem-in-uinavigationbar/33122015#33122015 – Zoltán Matók Oct 14 '15 at 09:48
  • I am also having issues with getting the arrow to stop jumping. @alku83 could you share your code? – Chris Brasino Jan 07 '16 at 21:26
  • Can anyone convert it in swift? – Rubaiyat Jahan Mumu Feb 13 '17 at 04:49
18

For those of you developing for iOS 5 who stumbled across this and were discouraged... Try something like this:

float my_offset_plus_or_minus = 3.0f;

UIBarButtonItem * item = [[UIBarButtonItem alloc] initWithTitle:@"title" 
                                                          style:UIBarButtonItemStyleDone
                                                          target:someObject action:@selector(someMessage)];

[item setBackgroundVerticalPositionAdjustment:my_offset_plus_or_minus forBarMetrics:UIBarMetricsDefault];
Chris Nolet
  • 8,714
  • 7
  • 67
  • 92
Joe Marx
  • 189
  • 1
  • 2
12

The best way is to subclass your UINavigationBar, as described here: https://stackoverflow.com/a/17434530/1351190

Here is my example:

#define NAVIGATION_BTN_MARGIN 5

@implementation NewNavigationBar

- (void)layoutSubviews {

    [super layoutSubviews];

    UINavigationItem *navigationItem = [self topItem];

    UIView *subview = [[navigationItem rightBarButtonItem] customView];

    if (subview) {

        CGRect subviewFrame = subview.frame;
        subviewFrame.origin.x = self.frame.size.width - subview.frame.size.width - NAVIGATION_BTN_MARGIN;
        subviewFrame.origin.y = (self.frame.size.height - subview.frame.size.height) / 2;

        [subview setFrame:subviewFrame];
    }

    subview = [[navigationItem leftBarButtonItem] customView];

    if (subview) {

        CGRect subviewFrame = subview.frame;
        subviewFrame.origin.x = NAVIGATION_BTN_MARGIN;
        subviewFrame.origin.y = (self.frame.size.height - subview.frame.size.height) / 2;

        [subview setFrame:subviewFrame];
    }
}

@end

Hope it helps.

Community
  • 1
  • 1
Simone Lai
  • 381
  • 3
  • 10
  • 1
    It works well but using a loop for all subviews is a not necessary action. You just can take `[[navigationItem leftBarButtonItem] customView]` or `[[navigationItem rightBarButtonItem] customView]` and set those frames directly. – Rostyslav Druzhchenko Jan 28 '15 at 15:09
6

Try the code below,

UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithTitle:@"Logout" style:UIBarButtonItemStyleDone target:self action:nil];
[button setBackgroundVerticalPositionAdjustment:-20.0f forBarMetrics:UIBarMetricsDefault];
[[self navigationItem] setRightBarButtonItem:button];

Its used to change the 'y' position in this code. Change the 'y' value (here it is -20.0f) according to your requirement. If the value is positive, it will down the button position. If the value is negative, it will up your button position.

Augustine P A
  • 5,008
  • 3
  • 35
  • 39
6

I needed to set my button to be more over towards the right. Here's how I did it using UIAppearance in Swift. There's a vertical position property there as well so I imagine you can adjust in any direction.

UIBarButtonItem.appearance().setTitlePositionAdjustment(UIOffset.init(horizontal: 15, vertical: 0), forBarMetrics: UIBarMetrics.Default)

This seems much less invasive to me than messing with the frame directly or adding custom subviews.

Mark Bridges
  • 8,228
  • 4
  • 50
  • 65
4

If you're simply using an image and NOT the default chrome, you can use negative image insets (set in the size inspector) to move your image around inside the UIBarButtonItem (handy because by default the horizontal padding can result in the image being further to the inside than you want). You can use the image insets to position the image outside of the bounds of the UIBarButtonItem, as well, and the entire vicinity of the left-hand-side button is tappable, so you don't have to worry about ensuring it's positioned near a tap target. (at least, within reason.)

Christopher Swasey
  • 10,392
  • 1
  • 31
  • 25
4

Navigation bar using change left bar position and image edge insets

swift 4

let leftBarButtonItem = UIBarButtonItem.init(image: UIImage(named:"ic_nav-bar_back.png"), landscapeImagePhone: nil, style: .plain, target: viewController, action: #selector(viewController.buttonClick(_:)))
            leftBarButtonItem.imageInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0)
            leftBarButtonItem.tintColor = UIColor(hex: 0xED6E19)
            viewController.navigationItem.setLeftBarButton(leftBarButtonItem, animated: true)
Srinivasan_iOS
  • 972
  • 10
  • 12
3

The best solution I could find is to initialize a UIBarButtonItem with a subview that includes extra space to the left/right. That way you wont have to worry about subclassing, and changing the layout of other elements inside the navigation bar, such as the title.

For example, to move a button 14 points to the left:

UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, image.size.width + 14, image.size.height)];
UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(-14, 0, image.size.width, image.size.height);
[button setImage:image forState:UIControlStateNormal];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
[containerView addSubview:button];

UIButton* button2 = [UIButton buttonWithType:UIButtonTypeCustom];
button2.frame = CGRectMake(0, 0, image.size.width + 14, image.size.height);
[button2 addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
[containerView addSubview:button2];

UIBarButtonItem* item = [[[self alloc] initWithCustomView:containerView] autorelease];
Jorge Aguirre
  • 2,787
  • 3
  • 20
  • 27
3

Swift 3.1

let cancelBarButtonItem = UIBarButtonItem()
cancelBarButtonItem.setBackgroundVerticalPositionAdjustment(4, for: .default)
vc.navigationItem.setLeftBarButton(cancelBarButtonItem, animated: true)
Adam Smaka
  • 5,977
  • 3
  • 50
  • 55
3
  • Swift 3
  • custom navigation bar height
  • no title jumping

Step 1: Set title position using Appearance API. For example, in AppDelegate's didFinishLaunchingWithOptions

UINavigationBar.appearance().setTitleVerticalPositionAdjustment(-7, for: .default)

Step 2: Subclass UINavigationBar

class YourNavigationBar: UINavigationBar {

    let YOUR_NAV_BAR_HEIGHT = 60

    override func sizeThatFits(_ size: CGSize) -> CGSize {
        return CGSize(width: UIScreen.main.bounds.width, 
                     height: YOUR_NAV_BAR_HEIGHT)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let navigationItem = self.topItem

        for subview in subviews {
            if  subview == navigationItem?.leftBarButtonItem?.customView ||
                subview == navigationItem?.rightBarButtonItem?.customView {
                subview.center = CGPoint(x: subview.center.x, y: YOUR_NAV_BAR_HEIGHT / 2)
            }
        }
    }
}
andriy_fedin
  • 175
  • 2
  • 9
3

Here's Adriano's solution using Swift 3. It was the only solution that worked for me and I tried several.

  let suggestImage  = UIImage(named: "menu.png")!
    let suggestButton = UIButton(frame: CGRect(x:0, y:0, width:34, height:20))
    suggestButton.setBackgroundImage(suggestImage, for: .normal)
    suggestButton.addTarget(self, action: #selector(self.showPopover(sender:)), for:.touchUpInside)
    suggestButton.transform = CGAffineTransform(translationX: 0, y: -8)
    // add the button to a container, otherwise the transform will be ignored
    let suggestButtonContainer = UIView(frame: suggestButton.frame)
    suggestButtonContainer.addSubview(suggestButton)
    let suggestButtonItem = UIBarButtonItem(customView: suggestButtonContainer)
    // add button shift to the side
    navigationItem.leftBarButtonItem = suggestButtonItem
JP Aquino
  • 3,946
  • 1
  • 23
  • 25
3

In my case

  1. change barbuttonItem's frame to customize spaces

  2. Add, Remove barButtonItems dynamically.

  3. change tint colors by tableview's contentOffset.y

like this enter image description here enter image description here

enter image description here enter image description here

If your minimum target is iOS 11, you can change the barButton frames in the viewDidLayoutSubviews

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // Change the navigationBar item frames
    if let customView = wishButton.customView?.superview {
        customView.transform = CGAffineTransform(translationX: 7.0, y: 0)
    }

    if let customView = gourmetCountButton.customView?.superview {
        customView.transform = CGAffineTransform(translationX: 9.0, y: 0)
    }
}

But, it's Only worked on iOS 11.

I also tried using the fixedSpace. But It didn't work in multiple navigationBarButton items.

 let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
    space.width = -10

So, I changed customView's width to adjust horizontal space.

This is one of the my barButtonItem class

final class DetailShareBarButtonItem: UIBarButtonItem {

// MARK: - Value
// MARK: Public
***// Change the width to adjust space***
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 32.0, height: 30.0))

override var tintColor: UIColor? {
    didSet {
        button.tintColor = tintColor
    }
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setButton()
}

required override init() {
    super.init()
    setButton()
}


// MARK: - Function
// MARK: Private
private func setButton() {
    // Button
    button.setImage( #imageLiteral(resourceName: "navibarIcShare02White").withRenderingMode(.alwaysTemplate), for: .normal)
    button.tintColor              = .white
    button.imageEdgeInsets        = UIEdgeInsetsMake(0, 1.0, 1.0, 0)
    button.imageView?.contentMode = .scaleAspectFill

    let containerView = UIView(frame: button.bounds)
    containerView.backgroundColor = .clear
    containerView.addSubview(button)
    customView = containerView
}
}

This is the result.

I tested on iOS 9 ~ 11, (Swift 4)

enter image description here

Den
  • 3,179
  • 29
  • 26
2

As @Anomie said, we need to subclass UINavigationBar, and override layoutSubviews().

This will place all right bar button items firmly attached to the right side of the navigation bar (as opposed to being slightly left-adjusted by default):

class AdjustedNavigationBar: UINavigationBar {

    override func layoutSubviews() {
        super.layoutSubviews()

        if let rightItems = topItem?.rightBarButtonItems where rightItems.count > 1 {
            for i in 0..<rightItems.count {
                let barButtonItem = rightItems[i]
                if let customView = barButtonItem.customView {
                    let frame = customView.frame
                    customView.frame = CGRect(x: UIApplication.sharedApplication().windows.last!.bounds.size.width-CGFloat(i+1)*44, y: frame.origin.y, width: frame.size.width, height: frame.size.height)
                }

            }
        }
    }
}

The only place to set the UINavigationBar property of UINavigationController is in its init(), like so:

let controllerVC = UINavigationController(navigationBarClass: AdjustedNavigationBar.self, toolbarClass: nil)
controllerVC.viewControllers = [UIViewController()]

The second line sets the root view controller of UINavigationController. (Since we cannot set it via init(rootViewController:)

Zoltán Matók
  • 3,923
  • 2
  • 33
  • 64
  • You really shouldn't be using your window bounds as a reference point... `UIApplication.sharedApplication().windows.last!.bounds.size.width`. – Zorayr Feb 18 '16 at 02:08
2

You can always do adjustments using Insets on the button. For example,

UIButton *toggleBtn =  [UIButton buttonWithType:UIButtonTypeCustom];
[toggleBtn setFrame:CGRectMake(0, 0, 20, 20)];
[toggleBtn addTarget:self action:@selector(toggleView) forControlEvents:UIControlEventTouchUpInside];

[toggleBtn setImageEdgeInsets:((IS_IPAD)? UIEdgeInsetsMake(0,-18, 0, 6) : UIEdgeInsetsMake(0, -3, 0, -3))];

UIBarButtonItem *toggleBtnItem = [[UIBarButtonItem alloc] initWithCustomView: toggleBtn];
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:searchBtnItem, toggleBtnItem, nil];

It works for me.

Joe M
  • 669
  • 6
  • 9
1

Init UIBarButtonItem with custom view and overload layoutSubviews in custom view, like this

-(void) layoutSubviews {
  [super layoutSubviews];
  CGRect frame = self.frame;
  CGFloat offsetY = 5;
  frame.origin.y = (44 - frame.size.height) / 2 - offsetY;
  self.frame = frame;
}
alex1704
  • 479
  • 5
  • 8
1

If you're only looking for adjusting the position of the customized back button like me, I used one of the solutions that add insets to the UIImage itself to do achieve this.

I used the solution from: https://stackoverflow.com/a/31240900/1241783

Add this extension function

import UIKit

extension UIImage {
    func imageWithInsets(insets: UIEdgeInsets) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(
            CGSize(width: self.size.width + insets.left + insets.right,
                   height: self.size.height + insets.top + insets.bottom), false, self.scale)
        let _ = UIGraphicsGetCurrentContext()
        let origin = CGPoint(x: insets.left, y: insets.top)
        self.draw(at: origin)
        let imageWithInsets = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return imageWithInsets
    }
}

then use it like such when customizing the back button

let backIcon = UIImage(named: "back_btn_icon")!.imageWithInsets(insets: UIEdgeInsets(top: 0, left: 0, bottom: 5, right: 0))
navigationController?.navigationBar.backIndicatorImage = backIcon
navigationController?.navigationBar.backIndicatorTransitionMaskImage = backIcon

Adjust the insets as you desire

Bruce
  • 2,357
  • 5
  • 29
  • 50
0

I found the solution of this problem by making adjustment in the Image Edge Insets of the custom button. I had the requirement in the app to increase the height of the navigation bar and after increasing the height makes the rightBarButtonItem and leftBarButtonItem images unpositioned problem.

Find the code below:-

UIImage *image = [[UIImage imageNamed:@"searchbar.png"];
UIButton* searchbutton = [UIButton buttonWithType:UIButtonTypeCustom];
[searchbutton addTarget:self action:@selector(searchBar:) forControlEvents:UIControlEventTouchUpInside]; 
searchbutton.frame = CGRectMake(0,0,22, 22);
[searchbutton setImage:image forState:UIControlStateNormal];
[searchbutton setImageEdgeInsets:UIEdgeInsetsMake(-50, 0,50, 0)];
// Make BarButton Item
 UIBarButtonItem *navItem = [[UIBarButtonItem alloc] initWithCustomView:searchbutton];
self.navigationItem.rightBarButtonItem = navItem;

Hope this helps anyone.

iGW
  • 633
  • 5
  • 14
0

Affine transform can do what you need. In my case designer gave me 16x16 close icon and I want to create 44x44 tap area.

closeButton.transform = CGAffineTransform(translationX: (44-16)/2, y: 0)
closeButton.snp.makeConstraints { make in
   make.size.equalTo(CGSize(width: 44, height: 44))
}
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: closeButton)
vdugnist
  • 371
  • 2
  • 7
0

Here is a simple workaround that was sufficient for my needs. I added an info button on the right hand side of the UINavigationBar but by default it sits way too close to the edge. By extending the width of the frame I was able to create the extra spacing needed on the right.

 UIButton *info = [UIButton buttonWithType:UIButtonTypeInfoLight];
 CGRect frame = info.frame;
 frame.size.width += 20;
 info.frame = frame;
 myNavigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc]initWithCustomView:info]autorelease];
anna
  • 2,723
  • 4
  • 28
  • 37