53

I am using the following codes to add two button to self.navigationItem.rightBarButtonItems, and I think in iOS7, the space between two buttons are too wide, is there a way to decrease the space between these two buttons?

UIBarButtonItem *saveStyleButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"save.png"] style:UIBarButtonItemStyleBordered target:self action:@selector(saveStyle)];

UIBarButtonItem *shareStyleButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(shareStyle)];

NSArray *arr= [[NSArray alloc] initWithObjects:shareStyleButton,saveStyleButton,nil];

self.navigationItem.rightBarButtonItems=arr;

Appreciate any hint or idea.

Bob
  • 2,263
  • 5
  • 21
  • 30

15 Answers15

56

Updated at Jul 2015

A better way to do this is to use storyboard (tested in Xcode 6.4). First, add a UINavigationItem; secondly, add a Bar Button Item; thirdly, add a view to the Bar Button Item you just created in step 2; fourthly, add as many buttons as you wish into that view you just dragged in; lastly, adjust the space with your mouse and constraints.

Related Questions

Can't assign multiple Buttons to UINavigationItem when using Storyboard with iOS 5

How to add buttons to navigation controller visible after segueing?


Old Answer (Only acceptable for small insets)

Use imageInsets property:

leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);
rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);

for three or more buttons, the middle one(s) get both insets:

leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);
middleButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, -15);
rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);

For the right side buttons, be careful: the FIRST button in the item array is the RIGHT one:

rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);
middleButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, -15);
leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);

IMPORTANT: Split the inset between the two neighbors; if apply the entire inset to one edge, it will become obvious that the buttons are overlapping in the "blank" space - one button gets all of the "gap" touches. Even when "split" the adjustment like this, at -40 on both edges, the tap will definitely go to wrong button sometimes. -15 or -20 is the most to consider using with this technique.

By applying this method, the button could even be moved around in four directions.

Community
  • 1
  • 1
Bob
  • 2,263
  • 5
  • 21
  • 30
  • 28
    While this will move the image, it does not move the tap-able area. – tybro0103 Jun 28 '14 at 14:58
  • 4
    To preserve the tap-able area you want to edit the imageInsets on both the left and right UIBarButtonItem like this: `firstButton.imageInsets = UIEdgeInsetsMake(0, 0, 0, -25); secondButton.imageInsets = UIEdgeInsetsMake(0, -25, 0, 0);` – Aron Lindberg Oct 22 '14 at 11:47
  • Tried negative insets, visuals are properly adjusted; but, tap is not accurate. E.g. If you have 3 button and used the imageInsets to make the spacing tighter, the tap on button is not mapped to the actual button. – Ramesh May 04 '15 at 18:11
  • It worked perfectly with Storyboard and UIBarbuttonItem with customView – Alphonse R. Dsouza Nov 04 '15 at 14:54
  • 1
    FWIW: the old approach, negativeInsets, are a tolerable hack for small value such as -10, but for larger values you soon discover that the buttons are actually overlapping: one of the buttons is "in front", and gets the tap anywhere in the overlapping area. I mention this because if one only needs a small tweak, setting negativeInset is easy to do in code, so is tempting - just don't adjust too far. – ToolmakerSteve Mar 18 '17 at 05:29
  • @AronLindberg - "splitting" the image-insets between the buttons does help, but be aware that the buttons do still end up with some overlap. This can be seen with a large offset, especially in emulator, where can use a mouse to see exactly where the touch is. The technique seems fine for -10 from each side, but is borderline at -25. At -40, its a definite fail in my tests; with three buttons, the middle one is receiving tap over the entire (small) visual gap - even slightly into the edge of neighbor button's icon. emulator, iOS 10.2, iPhone SE, 44 pixel icons (22@x2). – ToolmakerSteve Mar 18 '17 at 05:45
  • The updated part here worked perfectly for me, as I only needed the logo image to display on the right, and not in center as title. And simply disabling the button did the trick. thumbs up @Bob – Slobodan Antonijević May 31 '17 at 23:25
  • @AronLindberg **reduce space between 3 buttons.** I've 3 right bar buttons I want reduce the default space between them, all 3 created programmatically in Objective C. When I add this values it increases the space between buttons and if i make -15 to -5 it was space as like default space is there any way to reduce space between 3 buttons????. **firstButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0); secondButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, -15); thirdButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15); ** – iOSDude Sep 29 '21 at 18:07
53

My solution is using a custom view for right bar buttons. Create a horizontal stackview with equal spacing and add any number of buttons as subview.

Sample code:

func addRightBarButtonItems()
{
    let btnSearch = UIButton.init(type: .custom)
    btnSearch.setImage(UIImage(systemName: "magnifyingglass"), for: .normal)
    btnSearch.addTarget(self, action: #selector(MyPageContainerViewController.searchButtonPressed), for: .touchUpInside)
    
    let btnEdit = UIButton.init(type: .custom)
    btnEdit.setImage(UIImage(systemName: "pencil"), for: .normal)
    btnEdit.addTarget(self, action: #selector(MyPageContainerViewController.editButtonPressed), for: .touchUpInside)
    
    let stackview = UIStackView.init(arrangedSubviews: [btnEdit, btnSearch])
    stackview.distribution = .equalSpacing
    stackview.axis = .horizontal
    stackview.alignment = .center
    stackview.spacing = 8
    
    let rightBarButton = UIBarButtonItem(customView: stackview)
    self.navigationItem.rightBarButtonItem = rightBarButton
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Ali Seymen
  • 771
  • 6
  • 9
24

Swift 5

In your AppDelegate add this code:

let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
stackViewAppearance.spacing = -10

This will work with no additional code in more recent SDK versions as UIBarButtonItems are already contained in a horizontal UIStackView

Matt Long
  • 24,438
  • 4
  • 73
  • 99
haeckmaenn
  • 241
  • 2
  • 3
  • 1
    This answer is cute at first but then the downsides are realized: (1) Apple doesn't document that bar button items are implemented as UIStackView, and so they can change that at anytime without warning... no deprecations, no explanation... etc... then you have to rebuild your app and may have some head scratching to do to solve a problem correctly you should have solved right in the first place. Also, this is a one size fits all screens approach, and that app-wide spacing may not be right for every screen. So I am opting for @AliSeymen's answer – clearlight Sep 07 '22 at 20:39
16

First:

For UIBarButtonItem you must use constructor init(customView: UIView)

Second:

Use fixedSpace for set space between buttons

example:

let firstButton = UIButton()
let firstButtonItem = UIBarButtonItem(customView: firstButton)

let secondButton = UIButton()
let secondButtonItem = UIBarButtonItem(customView: secondButton)

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

self.navigationItem.rightBarButtonItems = [firstButtonItem, space, secondButtonItem]
ober
  • 2,363
  • 1
  • 19
  • 17
  • 1
    This is a good solution. However, if the `title` of view controller is too big this spacing is neglected. – Rishab Mar 04 '19 at 11:52
10

One line of code is all you need to decrease the space between buttons in the navigation bar:

UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).spacing = -10

You must place this line in your code before you add the buttons to the navigation bar.

Ely
  • 8,259
  • 1
  • 54
  • 67
6

If you are looking to have 2 buttons on the top right with no space in between them or on the right, this has worked for me.

let imgLeft = UIImage(named: "buttonLeft")?.imageWithRenderingMode(.AlwaysOriginal)
let bLeft = UIBarButtonItem(image: imgLeft, style: UIBarButtonItemStyle.Done, target: self, action: "action1:")
let space = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
space.width = -16.0

bLeft.imageInsets = UIEdgeInsetsMake(0, 0, 0, -25.0)


let imgRight = UIImage(named: "buttonRight")?.imageWithRenderingMode(.AlwaysOriginal)
let bRight = UIBarButtonItem(image: imgRight, style: UIBarButtonItemStyle.Done, target: self, action: "action2:")

bRight.imageInsets = UIEdgeInsetsMake(0, -25, 0, 0)


self.navigationItem.rightBarButtonItems = [space,bLeft,bRight ]
abinop
  • 3,153
  • 5
  • 32
  • 46
4

My situation was about giving horizontal space to logOut Button to the right edge.

 func addLogOutButtonToNavigationBar(triggerToMethodName: String)
    {
        let button: UIButton = UIButton()
        button.setImage(UIImage(named: "logOff.png"), forState: .Normal)
        button.frame = CGRectMake(20, 0, 30, 25)
        button.contentEdgeInsets = UIEdgeInsets.init(top: 0, left: 10, bottom: 0, right: -10)

        button .addTarget(self, action:Selector(triggerToMethodName), forControlEvents: UIControlEvents.TouchUpInside)
        let rightItem:UIBarButtonItem = UIBarButtonItem()
        rightItem.customView = button
        self.navigationItem.rightBarButtonItem = rightItem
    }
dumbledad
  • 16,305
  • 23
  • 120
  • 273
Alvin George
  • 14,148
  • 92
  • 64
4

Might be a bit late for this answer however this can help the newest IOS+Swift combination (IOS 10 and Swift 3 in my case). Here I describe a general approach for how to move items right/left for rightBarButtonItems/leftBarButtonItems:

The property you we have use here to move a barButtonItem is "imageEdgeInsets" . So, Here how to use this property -

yourBarButtonItem.imageEdgeInsets = UIEdgeInsetsMake(top, left, bottom, right)

These top, left, bottom, right are of type CGFloat and these are basically margin value that pushes your item from/to each other. For decreasing a space, we can just use minus (-) values like this " -10 ".

So, for example if we want to use this for a group of leftBatButtonItems and say, if we want to move a item to the a bit right, then we can do this -

ourBarButtonItem.imageEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, -15)

I hope we get the general idea here and hope it helps :)

sahilabrar
  • 638
  • 1
  • 8
  • 15
3

Without any code. I just put another UIBarButtonItem in-between the buttons that need spacing in storyboard. The button is just a placeholder for spacing and the UIBarButton should have UIView as subview of the UIBarButtonItem. adjust the view's width for your spacing. See Screen shots.

enter image description here

enter image description here

enter image description here

iOSCodeJunkie
  • 191
  • 1
  • 4
2

Create a UIBarButtonItem with type flexible or fixed space. Set the width and add it to the array of barbuttonitems. Try using a negative width, see if that works.

Or, you could maybe adjust your image. The system buttons i think have a fixed size, and might include some transparent part, so even when packed together the still seem spaced.

Hunter Monk
  • 1,967
  • 1
  • 14
  • 25
Joride
  • 3,722
  • 18
  • 22
  • 1
    Tried both, but neither works. It seems that there is a minimal value which couldn't be overwritten. – Bob Mar 30 '14 at 08:54
  • Maybe try wit custom view see how small you can make that custom view to find out if there is a lower limit. I guess that the system ones are just as they are and pretty much immutable (unless you hack something maybe). – Joride Mar 30 '14 at 08:57
  • holy crap, i solve it myself with just one more line of code! – Bob Mar 30 '14 at 09:21
  • 1
    The code is saveStyleButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -50); – Bob Mar 30 '14 at 09:21
  • And I think by applying this method the button could be even be moved around in four directions! what a bonus point! – Bob Mar 30 '14 at 09:28
2

Swift 5

If you want to add space between two Bar Button items then add a flexible space in between, the two buttons will be pushed to the left and right edge as the flexible space expands to take up most of the toolbar.

For Example:

let toolBar = UIToolbar()

var items = [UIBarButtonItem]()

let backBarButton =  UIBarButtonItem(image: UIImage(named: "icon-back.png"), style: .done, target: self, action: #selector(backButtonTapped))

let nextBarButton =  UIBarButtonItem(image: UIImage(named: "icon-next.png"), style: .done, target: self, action: #selector(nextButtonTapped))

let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)

items.append(backBarButton)
items.append(spacer)
items.append(nextBarButton)

toolBar.setItems(items, animated: true)
bona912
  • 589
  • 2
  • 6
  • 14
1

To accomplish this in code without adding an extra container view, use a UIBarButtonItem with the system item type set to FixedSpace. Then set the width of the fixed space to -10 and place it between the two buttons.

Alex311
  • 378
  • 3
  • 12
  • 4
    Trying on iOS 10.2: positive values of width work, the neighboring buttons are farther apart, but negative value has same result as zero value: the neighboring buttons are at the same locations as if the FixedSpace spacer was not there. I suspect they have nullified this ability to overlap items. – ToolmakerSteve Mar 18 '17 at 05:23
  • This works for swift 4 if the numbers are positive. – Micah Montoya Apr 13 '19 at 01:15
1

another answer : It works in ios 9 - 12. You should call fixNavigationItemsMargin(margin:) in function viewDidAppear(_ animated: Bool) and viewDidLayoutSubviews(). fixNavigationItemsMargin(margin:) would modify the UINavigationController stack.

you could call fixNavigationItemsMargin(margin:) in BaseNavigationController ,do the common work. And call fixNavigationItemsMargin(margin:) in UIViewController do precise layout.

// do common initilizer
class BaseNavigationController: UINavigationController {
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        fixNavigationItemsMargin()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        fixNavigationItemsMargin()
    }

}

extension UINavigationController {
func fixNavigationItemsMargin(_ margin: CGFloat = 8) {
    let systemMajorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
    if systemMajorVersion >= 11 {
        // iOS >= 11
        guard let contentView = navigationBar.subviews
            .first(
                where: { sub in
                    String(describing: sub).contains("ContentView")
            }) else { return }

        // refer to: https://www.matrixprojects.net/p/uibarbuttonitem-ios11/
        // if rightBarButtonItems has not any custom views, then margin would be 8(320|375)/12(414)
        // should use customView
        let needAdjustRightItems: Bool
        if let currentVC = viewControllers.last,
            let rightItems = currentVC.navigationItem.rightBarButtonItems,
            rightItems.count > 0,
            rightItems.filter({ $0.customView != nil }).count > 0 {
            needAdjustRightItems = true
        } else {
            print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
            needAdjustRightItems = false
        }

        let needAdjustLeftItems: Bool
        if let currentVC = viewControllers.last,
            let leftItems = currentVC.navigationItem.leftBarButtonItems,
            leftItems.count > 0,
            leftItems.filter({ $0.customView != nil }).count > 0 {
            needAdjustLeftItems = true
        } else {
            print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
            needAdjustLeftItems = false
        }

        let layoutMargins: UIEdgeInsets
        if #available(iOS 11.0, *) {
            let directionInsets = contentView.directionalLayoutMargins
            layoutMargins = UIEdgeInsets(
                top: directionInsets.top,
                left: directionInsets.leading,
                bottom: directionInsets.bottom,
                right: directionInsets.trailing)
        } else {
            layoutMargins = contentView.layoutMargins
        }

        contentView.constraints.forEach(
            { cst in

                // iOS 11 the distance between rightest item and NavigationBar should be  margin
                // rightStackView trailing space is -margin / 2
                // rightestItem trailing to rightStackView trailing is -margin / 2
                let rightConstant = -margin / 2

                switch (cst.firstAttribute, cst.secondAttribute) {
                case (.leading, .leading), (.trailing, .trailing):

                    if let stackView = cst.firstItem as? UIStackView,
                        stackView.frame.minX < navigationBar.frame.midX {
                        // is leftItems
                        if needAdjustLeftItems {
                            cst.constant = margin - layoutMargins.left
                        }
                    } else if let layoutGuide = cst.firstItem as? UILayoutGuide,
                        layoutGuide.layoutFrame.minX < navigationBar.frame.midX {
                        // is leftItems
                        if needAdjustLeftItems {
                            cst.constant = margin - layoutMargins.left
                        }
                    }

                    if let stackView = cst.firstItem as? UIStackView,
                        stackView.frame.maxX > navigationBar.frame.midX {
                        // is rightItems
                        if needAdjustRightItems {
                            cst.constant = rightConstant
                        }
                    } else if let layoutGuide = cst.firstItem as? UILayoutGuide,
                        layoutGuide.layoutFrame.maxX > navigationBar.frame.midX {
                        // is rightItems
                        if needAdjustRightItems {
                            cst.constant = rightConstant
                        }
                    }

                default: break
                }

        })

        // ensure items space == 8, minispcae
        contentView.subviews.forEach(
            { subsub in
                guard subsub is UIStackView else { return }
                subsub.constraints.forEach(
                    { cst in
                        guard cst.firstAttribute == .width
                            || cst.secondAttribute == .width
                            else { return }
                        cst.constant = 0
                })
        })

    } else {
        // iOS < 11

        let versionItemsCount: Int
        if systemMajorVersion == 10 {
            // iOS 10 navigationItem.rightBarButtonItems == 0
            // space = 16(320|375) / 20(414)
            // should adjust margin
            versionItemsCount = 0
        } else {
            // iOS 9 navigationItem.rightBarButtonItems == 0
            // space = 8(320|375) / 12(414)
            // should not adjust margin
            versionItemsCount = 1
        }

        let spaceProducer = { () -> UIBarButtonItem in
            let spaceItem = UIBarButtonItem(
                barButtonSystemItem: .fixedSpace,
                target: nil,
                action: nil)
            spaceItem.width = margin - 16
            return spaceItem
        }
        if let currentVC = viewControllers.last,
            var rightItems = currentVC.navigationItem.rightBarButtonItems,
            rightItems.count > versionItemsCount,
            let first = rightItems.first {

            // ensure the first BarButtonItem is NOT fixedSpace
            if first.title == nil && first.image == nil && first.customView == nil {
                print("rightBarButtonItems SPACE SETTED!!!  SPACE: ", abs(first.width))

            } else {
                rightItems.insert(spaceProducer(), at: 0)

                // arranged right -> left
                currentVC.navigationItem.rightBarButtonItems = rightItems
            }
        }

        if let currentVC = viewControllers.last,
            var leftItems = currentVC.navigationItem.leftBarButtonItems,
            leftItems.count > versionItemsCount,
            let first = leftItems.first {
            if first.title == nil && first.image == nil && first.customView == nil {
                print("leftBarButtonItems  SPACE SETTED!!!  SPACE: ", abs(first.width))
            } else {
                leftItems.insert(spaceProducer(), at: 0)

                // arranged left -> right
                currentVC.navigationItem.leftBarButtonItems = leftItems
            }
        }
    }
}
}

// do precise layout
class ViewController: UIViewController {
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    navigationController?.fixNavigationItemsMargin(40)
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    navigationController?.fixNavigationItemsMargin(40)
}
Jules
  • 631
  • 6
  • 14
  • This is so perfect! Thank you for this. Such a shame that an experienced iOS engr like me struggles with handling back buttons in iOS whenever the project has so multiple custom nav bar designs! Although I had to adjust and test lots of times my implementation using this code for different iOS versions. Thanks again. – Glenn Posadas Nov 11 '18 at 17:52
0

Found a crazy idea that works.

func createCustomToolbar(items: [UIBarButtonItem]) -> UIToolbar
{
    // no spacing between bar buttons
    let customToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: items.count*45, height: 44))
    customToolbar.items = items
    customToolbar.barStyle = UIBarStyle(rawValue: -1)!
    customToolbar.clearsContextBeforeDrawing = false
    customToolbar.backgroundColor = UIColor.clearColor()
    customToolbar.tintColor = UIColor.clearColor()
    customToolbar.translucent = true
    return customToolbar
}


let customToolbar = createCustomToolbar([item0,item1,item2,item3])
navigationItem.rightBarButtonItems = [UIBarButtonItem(customView: customToolbar)]

Tested on iOS7 and upper. Even this is written in swift the concept is clear.

Krešimir Prcela
  • 4,257
  • 33
  • 46
0

I gave up with fighting this bug, and came up with the following extension:

import UIKit

extension UIBarButtonItem {

    convenience init(buttonImage: UIImage?, target: Any?, action: Selector?) {
        let button = UIButton(type: .system)
        button.frame = CGRect(origin: CGPoint.zero, size: buttonImage != nil ? buttonImage!.size : CGSize.zero)
        button.setImage(buttonImage, for: .normal)

        if let action = action {
            button.addTarget(target, action: action, for: .touchUpInside)
        }

        self.init(customView: button)
    }

    public func updateButton(image: UIImage?) {
        if let button = self.customView as? UIButton {
            button.setImage(image, for: .normal)

            let size = image != nil ? image!.size : CGSize.zero
            let frame = button.frame
            button.frame = frame.insetBy(dx: ceil((frame.size.width - size.width) / 2), dy: ceil((frame.size.height - size.height) / 2))
        }
    }
}
bteapot
  • 1,897
  • 16
  • 24