iOS 11 UINavigationBar bar button items alignment

2020-05-18 11:24发布

I am upgrading my application to iOS 11 and I am seeing some problems with the navigation bar, part of my problems have already asked questions here, so I won't mention them in this question.

The particular problem in question is that the navigation bar's bar button items are spaced differently. My main left and right bar button items are closer to the screen's horizontal center now and I can't move them near to the screen edges. In the past I have used a custom UIButton subclass and created bar button items with custom view. The alignment solution were alignmentRectInsets and contentEdgeInsets, now I couldn't manage to produce the expected results using this approach.

Edit:
I have retested with iOS 11 beta 2 and the issue remains.

Edit 2: I Have retested with iOS beta 3 and the issue remains.

标签: ios11
10条回答
▲ chillily
2楼-- · 2020-05-18 11:43

Now in iOS 11 you can manage UINavigationBarContentView to adjust left and right constraints, and UIStackView to adjust buttons (or other elements).

This is my solution for a navigation bar with items on left and right. Also it fixes if you have several buttons together in one side.

- (void) updateNavigationBar {
    for(UIView *view in self.navigationBar.subviews) {
        if ([NSStringFromClass([view class]) containsString:@"ContentView"]) {

            // Adjust left and right constraints of the content view 
            for(NSLayoutConstraint *ctr in view.constraints) {
                if(ctr.firstAttribute == NSLayoutAttributeLeading || ctr.secondAttribute == NSLayoutAttributeLeading) {
                    ctr.constant = 0.f;
                } else if(ctr.firstAttribute == NSLayoutAttributeTrailing || ctr.secondAttribute == NSLayoutAttributeTrailing) {
                    ctr.constant = 0.f;
                }
            }

            // Adjust constraints between items in stack view
            for(UIView *subview in view.subviews) {
                if([subview isKindOfClass:[UIStackView class]]) {
                    for(NSLayoutConstraint *ctr in subview.constraints) {
                        if(ctr.firstAttribute == NSLayoutAttributeWidth || ctr.secondAttribute == NSLayoutAttributeWidth) {
                            ctr.constant = 0.f;
                        }
                    }
                }
            }
        }
    }
}


As you see it is not necessary to add more constraints, as other people have done. There are already defined constraints so they can be changed.

查看更多
该账号已被封号
3楼-- · 2020-05-18 11:43

Solution 1: In light of Apple's response that this is expected behavior, we worked around the problem by removing the toolbar and adding a custom view.

I realize that this may not be possible in all cases, but the regular UIView is much easier to customize to the app's appearance than the toolbar and navigation bar where Apple has control of the button positioning.

Instead of setting our custom button as the custom view of the ui bar button object, it was we set it as a subview of the blank ui buttons in the custom view.

Doing this we were able to return to the same look of our ios 10 app

Solution 2: A bit messy, we wrapped our custom view button in an outer UIButton so the frame location can be set. This does make the outer left edge of the button un-tappable unfortunately, but corrects the look of the button position. See example:

UIButton* outerButton = [UIButton new]; //the wrapper button
BorderedButton* button = [self initBorderedButton]; //the custom button
[button setTitle:label forState:UIControlStateNormal];
[button setFrame:CGRectMake(-10, 0, [label length] * 4 + 35, 30)];
[button addTarget:controller action:@selector(popCurrentViewController) forControlEvents:UIControlEventTouchUpInside];
[outerButton addSubview:button]; //add custom button to outer wrapper button
[outerButton setFrameWidth:button.frameWidth]; //make sure title gives button appropriate space
controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:outerButton]; //add wrapper button to the navigation bar
controller.navigationItem.hidesBackButton = YES;

Using that method we keep our navigation bar and can position the button to where it is needed.

Edit: We found that solution 2 does not work on ios 10, this probably will affect only the tiny percent of developers forced to be backward compatible.

Solution 3

What we were really more concerned about with the inward crowding of the buttons was the fact that the title of the navigation bar ran into the custom left button, the size of the margins was less important and used as a tool to make space. The solution is to simply add a spacer item to the left items.

UIBarButtonItem* backButton = [[UIBarButtonItem alloc] initWithCustomView:button];
UIBarButtonItem* spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
controller.navigationItem.leftBarButtonItems = [NSArray arrayWithObjects:backButton, spacer, nil];
查看更多
ら.Afraid
4楼-- · 2020-05-18 11:44

Another answer may be help.

My solution : 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()
}
}

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

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
            }
        }
    }
}
}
查看更多
走好不送
5楼-- · 2020-05-18 11:48

I had the same issue. I had three buttons as right barbutton items on the navigation bar stack view. So, I have updated the insets of the subviews of navigation bar.

override func viewWillLayoutSubviews() {
                super.viewWillLayoutSubviews()
                for view in (self.navigationController?.navigationBar.subviews)! {
                    view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11)
                }
            }

Here 11 is the space which I needed. It can be anything as per your requirement. Also, if you want the buttons with 0 insets, replace view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11) with view.layoutMargins = UIEdgeInsets.zero

查看更多
Rolldiameter
6楼-- · 2020-05-18 11:53

After about two days, here is the simplest and safest solution I could come up with. This solution only works with custom view bar button items, the code for which is included. It is important to note that the left and right margins on the navigation bar have not changed from iOS10 to iOS11 - they are still 16px. Such a large margin makes it difficult to have a sufficiently large click region.

Bar buttons are now layed out with UIStackView's. The prior method of shifting those buttons closer to the margin involved using negative fixed spaces which these stack views cannot handle.

Subclass UINavigationBar

FWNavigationBar.h

@interface FWNavigationBar : UINavigationBar

@end

FWNavigationBar.m

#import "FWNavigationBar.h"

@implementation FWNavigationBar

- (void)layoutSubviews {
    [super layoutSubviews];

    if (@available(iOS 11, *)) {
        self.layoutMargins = UIEdgeInsetsZero;

        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass([subview class]) containsString:@"ContentView"]) {
                subview.layoutMargins = UIEdgeInsetsZero;
            }
        }
    }
}

@end

Using the UINavigationController

#import "FWNavigationBar.h"

UINavigationController *controller = [UINavigationController initWithNavigationBarClass:[FWNavigationBar class] toolbarClass:nil];
[controller setViewControllers:@[rootViewController] animated:NO];

Creating a UIBarButton

Place this code either in a UIBarButton category or in the file you plan on using the bar button which will return an identical looking bar button item using a UIButton.

+ (UIBarButtonItem *)barButtonWithImage:(UIImage *)image target:(id)target action:(SEL)action {
   UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
   //Note: Only iOS 11 and up supports constraints on custom bar button views
   button.frame = CGRectMake(0, 0, image.size.width, image.size.height);

   button.tintColor = [UIColor lightGrayColor];//Adjust the highlight color

   [button setImage:image forState:UIControlStateNormal];
   //Tint color only applies to this image
   [button setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateHighlighted];
   [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];

   return [[UIBarButtonItem alloc] initWithCustomView:button];
}

Setting the bar button in your controller

- (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationItem.leftBarButtonItem = [UIBarButtonItem barButtonWithImage:[UIImage imageNamed:@"your_button_image"] target:self action:@selector(leftButtonPressed)];
}

Lastly, I would recommend leaving the left and right margins at zero and just adjusting the position of the button within your image. This will allow for you to take advantage of the full clickable region up to the edge of the screen. The same goes for the height of your image - make sure the height is 44 points.

查看更多
地球回转人心会变
7楼-- · 2020-05-18 11:56

There is good article on this : http://www.matrixprojects.net/p/uibarbuttonitem-ios11/

Using this we can at least push rightbarbuttonitems to right till it leaves 8 pixel margin from trailing of the UINavigationBar.

Explained really well.

查看更多
登录 后发表回答