65

I create one UIToolbar with code and another with interface builder. But, found out the two toolbar having different left and right padding which shown below:

From Interface Builder:

enter image description here

From Code:

enter image description here

UIImage *buttonImage = [[UIImage imageNamed:@"button.png"] stretchableImageWithLeftCapWidth:10 topCapHeight:0];
UIButton *btnTest = [UIButton buttonWithType:UIButtonTypeCustom];
[btnTest setBackgroundImage:buttonImage forState:UIControlStateNormal];
[btnTest setTitle:@"Back" forState:UIControlStateNormal];   
[btnTest.titleLabel setFont:[UIFont boldSystemFontOfSize:13]];  
[btnTest setBackgroundImage:[imgToolbarButton stretchableImageWithLeftCapWidth:5 topCapHeight:0]  forState:UIControlStateNormal];
[btnTest addTarget:self action:@selector(clearDateEdit:) forControlEvents:UIControlEventTouchUpInside];
btnTest.frame = CGRectMake(0.0, 0.0, 50, 30);
UIBarButtonItem *btnTestItem = [[UIBarButtonItem alloc] initWithCustomView:btnTest];
[self.toolbar setItems:[NSArray arrayWithObjects:btnTestItem,nil]];
[btnTestItem release];

My question is how can I adjust the left and right padding of UIToolbar by code?

Update

I discovered this alignment issue only happen to UIBarButtonItem with customView of UIButton, the alignment is fine with UIBarButtonItem. Any idea what cause this or to resolve this.

The only solution I can think of right now is to manually set the frame.

TonyTakeshi
  • 5,869
  • 10
  • 51
  • 72
  • By padding do you mean the spacing between buttons? – peterp May 16 '11 at 17:46
  • 2
    @TonyMocha are you sure that it does look different if you just add the two buttons by code with one flexible space inbetween? – Nick Weaver May 16 '11 at 19:05
  • @peterp. left and right padding for the first and last button. – TonyTakeshi May 17 '11 at 06:54
  • @nick weaver. yeah. Not sure is only me. But somehow, both it seems this 2 toolbar, having different padding space. I am scratching my head. – TonyTakeshi May 17 '11 at 06:55
  • @TonyMocha Have a look at [nib2objc](https://github.com/akosma/nib2objc) and let it transform your nib file to readable objc code. May some property is changed which is not obvious. – Nick Weaver May 17 '11 at 07:15

11 Answers11

153

I had the same issue, and there's a neat trick you can do with a UIBarButtonSystemItemFixedSpace, add one of these with a negative width before your first button and after your last button and it will move the button to the edge.

For example, to get rid of the right hand margin add the following FixedSpace bar item as the last item:

Update for iOS 11 up to 13

  • Swift

    let negativeSeperator = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
    negativeSeperator.width = 12
    

    Width must be positive in Swift version


  • Objc

    UIBarButtonItem *negativeSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    negativeSeparator.width = -12;
    

The margins on the left and right are 12px.

Update for iOS7 - margins are 16px on iPhone and 20px on iPad!

Mamad Farrahi
  • 394
  • 1
  • 5
  • 20
Craig Mellon
  • 5,399
  • 2
  • 20
  • 25
  • The beauty with this, is that it works with settings the leftBarButtonItems (iOS5) of a navigationItem. You get to continue to use the stock navigation bar and just add in this negative spacer as the first button in the array of say, left buttons. Nice! – idStar Nov 09 '11 at 18:47
  • 4
    I had to use width of -5, but this worked well. For the right bar button, I had to add a negativeSeperator before AND after it for it to work. – Kevin Elliott Apr 18 '12 at 08:40
  • 1
    Yes, it works, but it looks like a workaround... isn't it there another solution? – Bogdan Sep 17 '12 at 07:04
  • 7
    Kevin Elliott and anyone else interested, you don't need a separator before and after, just before. The code I'm using is `[[self.navigationBar topItem] setRightBarButtonItems:@[negativeSeparator, customButton]];` with a -5 pt wide negativeSeparator. When added this way, the first one in the array is the rightmost, the second one is placed to its left. Think of the right bar button items as a stack and this "reverse" ordering makes sense. – JK Laiho Sep 18 '12 at 07:35
  • A little something to add: You can't set the width of a fixed space item to be negative in storyboard. It will revert back to zero. You need to do it in code. – Cezar Feb 07 '14 at 12:12
  • 1
    @craig-mellon For iPhone 6, the 16px doesn't work. It works with 20px. Whats the best solution to solve this, across all iPhone devices. – Vignesh PT Mar 03 '15 at 11:13
  • It works for me when I give value "12" not "-12". Thank You. – Dimple Shah Oct 10 '15 at 10:06
  • 2
    Use `self.navigationController.view.layoutMargins.right` for actual margin. IOS8+ – BugaBuga Dec 01 '16 at 15:08
  • Thanks, this is really helpful. I use Objective-C and I got 4 navigationBarButtonItems in the right bar button items array. In my case, they are too close, I need to increase the space between them. `UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; positiveSeparator.width = 18;` – Zhou Haibo Sep 07 '21 at 03:24
35

Before iOS 11

You had three possible solutions:

  • The first was using fixed a space item UIBarButtonItem(barButtonSystemItem: .fixedSpace …) with a negative width
  • The other was to override alignmentRectInsets on the custom view
  • The last one, if you use UIButton as your item's custom view, you can override contentEdgeInsets and hitTest(_:with:)

Absolutely, the first solution seems better than other

UIBarButtonItem *negativeSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
negativeSeparator.width = -12;

if you use swift, code will be look like this

var negativeSeparator = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
negativeSeparator.width = -12

After iOS 11(contain iOS 11)

unfortunately, all the solution can't be use in iOS 11

  • first solution: setting a negative width no longer works
  • second solution: override alignmentRectInsets will cause part of item no longer receives touches
  • the last solution: i think override hitTest(_:with:) is not a good idea, don't do this please!

You can also see some suggestion on the Developer Forums, but after you look all the comment, you will find suggestion on this topic is to create a custom UINavigationBar subclass and do something complexity.

Oh my god, All i want to do is to change the padding, not the navigation bar!

Fortunately, we can do this with a trick in iOS 11!

Here We Go:

1. create a custom button

  • set translatesAutoresizingMaskIntoConstraints as false
  • override alignmentRectInsets
    • item in right side use UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 8)
    • item in left side use UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8)

      if you don't know alignmentRectInsets, you can read this blog first

swift version

var customButton = UIButton(type: .custom)
customButton.overrideAlignmentRectInsets = UIEdgeInsets(top: 0, left: x, bottom: 0, right: -x) // you should do this in your own custom class
customButton.translatesAutoresizingMaskIntoConstraints = false;

objective-c version

UIButton *customButton = [UIButton buttonWithType:UIButtonTypeCustom];
customButton.overrideAlignmentRectInsets = UIEdgeInsetsMake(0, x, 0, -x); // you should do this in your own custom class
customButton.translatesAutoresizingMaskIntoConstraints = NO;

2. create an item with your custom button

swift version

var item = UIBarButtonItem(customView: customButton)

objective-c version

UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:customButton]

3. create a fixedSpace type UIBarButtonItem with positive width

set a positive value, not a negative value

swift version

var positiveSeparator = UIBarButtonItem(barButtonSystemItem:.fixedSpace, target: nil, action: nil)
positiveSeparator.width = 8

objective-c version

UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
positiveSeparator.width = 8;

4. set an array to leftitems or rightitems

fixedSpace type UIBarButton item must be the first element in array.

swift version

self.navigationItem.leftBarButtonItems = [positiveSeparator, item, ...]

objective-c version

self.navigationItem.leftBarButtonItems = @{positiveSeparator, item, ...}

Well Done

after you doing all of the steps, you will see your padding become smaller and type area seems correct!

If you find something wrong, please let me know! I will try my best to answer your question!

Note

Before iOS 11, you should care about the device's screen width; if the screen is 5.5 inch, the negative is -12 pt, on other screens is -8pt.

If you use my solution on iOS 11, you don't need to care about the device screen, just set 8pt, You should care about item's position in navigation bar, left side or right side, this will affect your custom view's alignmentRectInsets

Want more tap area

If you want to promise your tap area is larger than 44 * 44 , you can override method below

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGSize acturalSize = self.frame.size;
    CGSize minimumSize = kBarButtonMinimumTapAreaSize;
    CGFloat verticalMargin = acturalSize.height - minimumSize.height >= 0 ? 0 : ((minimumSize.height - acturalSize.height ) / 2);
    CGFloat horizontalMargin = acturalSize.width - minimumSize.width >= 0 ? 0 : ((minimumSize.width - acturalSize.width ) / 2);
    CGRect newArea = CGRectMake(self.bounds.origin.x - horizontalMargin, self.bounds.origin.y - verticalMargin, self.bounds.size.width + 2 * horizontalMargin, self.bounds.size.height + 2 * verticalMargin);
    return CGRectContainsPoint(newArea, point);
}
SketchK
  • 411
  • 4
  • 11
  • This works perfectly for me, except that the target area is super small making it almost unusable. – Eric Dec 06 '17 at 00:03
  • @Eric if you want to modify the tap area, you can override the button's methods like ` - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event` – SketchK Dec 15 '17 at 06:25
  • `overrideAlignmentRectInsets` is not an attribute and `alignmentRectInsets` is only available for overriding - see my answer – RichAppz Jan 24 '18 at 16:22
  • @Eric if you read my answer carefully,you will find that i told you to overwrite alignmentRectInset at the begin of ios 11 chapter – SketchK Jan 25 '18 at 14:12
  • I used customButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: -25, bottom: 0, right: 0) instead of overrideAlignmentRectInsets. And with this, i did not have to use positiveSeparator. – Sujal Aug 13 '18 at 05:16
9

This is a great question and even with the solutions provided - it did not resolve the problem on iOS11.

WORKING SOLUTION

If you create a new extension of UIButton:

class BarButton: UIButton {

    override var alignmentRectInsets: UIEdgeInsets {
        return UIEdgeInsetsMake(0, 16, 0, 16)
    }

}

Then when you use it in your layout:

lazy var btnLogin: BarButton = {
    let btn = BarButton(type: .custom)
    // Add your button settings

    btn.widthAnchor.constraint(equalToConstant: self.frame.size.width).isActive = true
    btn.heightAnchor.constraint(equalToConstant: 50).isActive = true
    btn.translatesAutoresizingMaskIntoConstraints = false
    return btn
}()

This solved a very annoying problem, if you have any issues give me a shout.

RichAppz
  • 1,520
  • 2
  • 14
  • 27
  • This is a much cleaner approach and should be the accepted solution – evanjmg Jan 24 '18 at 16:21
  • problem solved but with a side effect, if the background of the custom button is different with the navigationbar in the next/previous controller, then a clipped button will be presented when pushing/popping. – will wang Oct 18 '18 at 03:23
  • This is a good solution but for me the touch area was off. I had to add the "positiveSeparator" mentioned above to shift the touch area over. – Joe Jun 08 '19 at 23:02
3

I stumbled upon this when trying to center a toolbar as a subview of an UIButton. As the toolbar needed to be of small width, the best solution was a flexible button on each side:

UIBarButtonItem flexibleButtonItemLeft = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];

UIBarButtonItem centeredButtonItem = ...

UIBarButtonItem flexibleButtonItemRight = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];

UIToolbar toolbar = ...

toolbar.items = @[flexibleButtonItemLeft, centeredButtonItem, flexibleButtonItemRight];
Karmeye
  • 1,399
  • 1
  • 8
  • 16
3

use this button

UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                                                                           target:nil
                                                                           action:nil];
Madhu
  • 1,542
  • 1
  • 14
  • 30
2

You can change offset and width of your toolbar, if you want to use customview (initWithCustomView)

[myToolBar setFrame:CGRectMake(-10, 0, [UIScreen mainScreen].bounds.size.width+10, 44)];
Mihriban Minaz
  • 3,043
  • 2
  • 32
  • 52
  • I think it is a much cleaner solution to add the UIBarButtonSystemItemFixedSpace as mentioned above - that way you don't risk having the toolbar overlapping other controls in the UI, causing other problems. Especially if the toolbar is transparent. – Miros Nov 01 '13 at 19:07
2

I met the same issue. Finally, I solved this by subclassing UIToolbar and override the layout subviews method.

- (void)layoutSubviews {

  [super layoutSubviews];

  if (leftItem_ && leftItem_.customView
      && [leftItem_.customView isKindOfClass:[UIButton class]]) {
    CGRect newFrame = leftItem_.customView.frame;
    newFrame.origin.x = 0;   // reset the original point x to 0; default is 12, wired number
    leftItem_.customView.frame = newFrame;    
  }

  if (rightItem_ && rightItem_.customView
      && [rightItem_.customView isKindOfClass:[UIButton class]]) {
    CGRect newFrame = rightItem_.customView.frame;
    newFrame.origin.x = self.frame.size.width - CGRectGetWidth(newFrame);
    rightItem_.customView.frame = newFrame;
  }

}
Fourj
  • 1,817
  • 1
  • 18
  • 34
0

I think a tricky way is to use Aspects (or method swizzling),as blew:

[UINavigationBar aspect_hookSelector:@selector(layoutSubviews) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
        UINavigationBar *bar = info.instance;

        //[bar layoutSubviews];

        if ([bar isKindOfClass:[UINavigationBar class]]) {
            if (@available(iOS 11, *)) {
                bar.layoutMargins = UIEdgeInsetsZero;

                for (UIView *subview in bar.subviews) {
                    if ([NSStringFromClass([subview class]) containsString:@"ContentView"]) {
                        UIEdgeInsets oEdges = subview.layoutMargins;
                        subview.layoutMargins = UIEdgeInsetsMake(0, 0, 0, oEdges.right);
                    }
                }
            }
        }
    } error:nil];
ekingo Hu
  • 51
  • 4
0

Just add negative constants to your leading and trailing constraints of the UIToolbar. That way, the toolbar starts and ends outside of your view and therefore has less padding.

heyfrank
  • 5,291
  • 3
  • 32
  • 46
0

Finally for having customized background image for the UIBarButtonItem and to accomodate the alignment, I have abandon UIBarButtonItem and adding UIButton manually.

UIImage *buttonImage = [[UIImage imageNamed:@"button.png"] stretchableImageWithLeftCapWidth:10 topCapHeight:0];
UIButton *btnTest = [UIButton buttonWithType:UIButtonTypeCustom];
[btnTest setBackgroundImage:buttonImage forState:UIControlStateNormal];
[btnTest setTitle:@"Back" forState:UIControlStateNormal];   
[btnTest.titleLabel setFont:[UIFont boldSystemFontOfSize:13]];  
[btnTest setBackgroundImage:[imgToolbarButton stretchableImageWithLeftCapWidth:5 topCapHeight:0]  forState:UIControlStateNormal];
[btnTest addTarget:self action:@selector(clearDateEdit:) forControlEvents:UIControlEventTouchUpInside];
btnTest.frame = CGRectMake(0.0, 0.0, 50, 30);
[self.toolbar addSubview:btnTest];
[btnTestItem release];
TonyTakeshi
  • 5,869
  • 10
  • 51
  • 72
0

In my case, I need to decrease the left padding of the leftBarButtonItem and keep touches all over the area.

class FilterButton: Button {

override init() {
    super.init()
    setImage(UIImage(named: "filter"), for: .normal)
    backgroundColor = UIColor.App.ultraLightGray
    layer.cornerRadius = 10
    
    snp.makeConstraints {
        $0.size.equalTo(36)
    }
}

override var alignmentRectInsets: UIEdgeInsets {
    return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: -4)
}
}


let filterButton = FilterButton()
filterButton.addTarget(self, action: #selector(filterTapped(_:)), for: .touchUpInside)
let filterBarButton = UIBarButtonItem(customView: filterButton)
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
    
navigationItem.setLeftBarButtonItems([spacer, filterBarButton], animated: false)