20 Replies
      Latest reply on Nov 29, 2017 5:58 AM by SketchK
      master-nevi Level 1 Level 1 (0 points)

        There used to be a way, on iOS 10 and below, to shift/position custom-view-UIBarButtonItems set on the left/rightBarButtonItems closer to the screen edges using a negative spacer outlined here:

         

        This trick no longer works on iOS 11 with the new auto layout support in the navigation bar. Is there another means of shifting the left/right buttons closer to the screen edge?

        • Re: iOS 11 - UIBarButtonItem horizontal position
          master-nevi Level 1 Level 1 (0 points)

          More info:

          As of iOS 11 the left and right bar button items appear to be contained within subclasses of UIStackView (_UIButtonBarStackView) and those stack views are constrained to the navigation bar edges with constants of 16 pts. So now the question is, what's the UIKit friendly way of setting those constants. Is there a layoutMargin/safeAreaInset I can update?

           

          Left side:

          <NSLayoutConstraint:0x1c4486d10 UILayoutGuide:0x1c41bbf20'BackButtonGuide(0x159e05750)'.trailing >= _UINavigationBarContentView:0x159d3fe90.leading + 16   (active)>,
          <NSLayoutConstraint:0x1c4486c20 H:[UILayoutGuide:0x1c41bbf20'BackButtonGuide(0x159e05750)']-(0)-[UILayoutGuide:0x1c41bc9a0'LeadingBarGuide(0x159e05750)']   (active)>,
          <NSLayoutConstraint:0x1c4486220 _UIButtonBarStackView:0x159e099e0.leading == UILayoutGuide:0x1c41bc9a0'LeadingBarGuide(0x159e05750)'.leading   (active)>,
          

           

          Right side:

          <NSLayoutConstraint:0x1c4482120 UILayoutGuide:0x1c41b9d00'TrailingBarGuide(0x159e05750)'.trailing == _UINavigationBarContentView:0x159d3fe90.trailing - 16 priority:999   (active)>,
          <NSLayoutConstraint:0x1c429dd80 _UIButtonBarStackView:0x15b958aa0.trailing == UILayoutGuide:0x1c41b9d00'TrailingBarGuide(0x159e05750)'.trailing   (active)>,
          
            • Re: iOS 11 - UIBarButtonItem horizontal position
              master-nevi Level 1 Level 1 (0 points)

              UPDATE: In beta 2, UIBarButtonItems without custom views appear to be offset from the navigation bar edge by 8pts instead of 16pts. However UIBarButtonItems with custom views are still offset by 16 pts .

               

              The code below demonstrates this behavior by implementing the left button with [UIBarButtonItem initWithCustomView:] using a UIButton as a custom view and the right button with [UIBarButtonItem initWithTitle:style:target:action:]. The custom view button is configured with the same tap target area via contentEdgeInsets that's imposed by on the non-custom right button:

               

               

              self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:({
                      UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
                      [button setTitle:@"Test" forState:UIControlStateNormal];
                      button.contentEdgeInsets = UIEdgeInsetsMake(13, 8, 13, 8);
                      button;
                  })];
              
              self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Test" style:UIBarButtonItemStylePlain target:nil action:nil];
              
              
              
              
              
              
              
              
              
              
              
            • Re: iOS 11 - UIBarButtonItem horizontal position
              treekers Level 1 Level 1 (0 points)

              I am using iOS 11 beta 3 and also experiencing this spacing issue.  If I set the image insets I can get it to "look" aligned to the edge, but the touch events are still offset. If you've figured anything out I would greatly appreciate some help.

              • Re: iOS 11 - UIBarButtonItem horizontal position
                wang_ctg Level 1 Level 1 (0 points)

                Here's my workaround, tested on iOS 11 beta 4.

                 

                BarButtonView.h

                #import <UIKit/UIKit.h>
                
                typedef NS_ENUM(NSInteger, BarButtonViewPosition) {
                    BarButtonViewPositionLeft,
                    BarButtonViewPositionRight
                };
                
                @interface BarButtonView : UIView
                
                @property (nonatomic, assign) BarButtonViewPosition position;
                
                @end
                

                 

                BarButtonView.m

                #import "BarButtonView.h"
                
                @interface BarButtonView ()
                {
                    BOOL applied;
                }
                
                @end
                
                @implementation BarButtonView
                
                - (void)layoutSubviews
                {
                    [super layoutSubviews];
                
                    if (applied || [[[UIDevice currentDevice] systemVersion] doubleValue]  < 11)
                    {
                        return;
                    }
                
                    // Find the _UIButtonBarStackView containing this view, which is a UIStackView, up to the UINavigationBar
                    UIView *view = self;
                    while (![view isKindOfClass:[UINavigationBar class]] && [view superview] != nil)
                    {
                        view = [view superview];
                        if ([view isKindOfClass:[UIStackView class]] && [view superview] != nil)
                        {
                            if (self.position == BarButtonViewPositionLeft)
                            {
                                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeLeading multiplier:1.0 constant:8.0]];
                                applied = YES;
                            }
                            else if (self.position == BarButtonViewPositionRight)
                            {
                                [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-8.0]];
                                applied = YES;
                            }
                            break;
                        }
                    }
                }
                
                @end
                

                 

                Usage:

                - (void)setLeftBarButtonView:(UIView *)view
                {
                    if ([[[UIDevice currentDevice] systemVersion] doubleValue] >= 11)
                    {
                        BarButtonView *barBtnView = [[BarButtonView alloc] initWithFrame:view.frame];
                        [barBtnView setPosition:BarButtonViewPositionLeft];
                        [barBtnView addSubview:view];
                   
                        [self.navigationItem setLeftBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:barBtnView]];
                    }
                    else
                    {
                        UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL];
                        [space setWidth:-8];
                    
                        [self.navigationItem setLeftBarButtonItems:@[space,[[UIBarButtonItem alloc] initWithCustomView:view]]];
                    }
                }
                
                - (void)setRightBarButtonView:(UIView *)view
                {
                    if ([[[UIDevice currentDevice] systemVersion] doubleValue] >= 11)
                    {
                        BarButtonView *barBtnView = [[BarButtonView alloc] initWithFrame:view.frame];
                        [barBtnView setPosition:BarButtonViewPositionRight];
                        [barBtnView addSubview:view];
                   
                        [self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:barBtnView]];
                    }
                    else
                    {
                        UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL];
                        [space setWidth:-8];
                    
                        [self.navigationItem setRightBarButtonItems:@[space,[[UIBarButtonItem alloc] initWithCustomView:view]]];
                    }
                }
                

                 

                This is definitely not the perfect solution, and this trigger the "Unable to simultaneously satisfy constraints" error in the debug console. Use with caution.

                  • Re: iOS 11 - UIBarButtonItem horizontal position
                    gergesrgs Level 1 Level 1 (0 points)

                    if (self.position == BarButtonViewPositionLeft){

                      [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading /

                                    applied = YES;

                                }

                        ->>

                    if (self.position == BarButtonViewPositionLeft){

                    for(NSLayoutConstraint *ctr in view.constraints) {

                                        if(ctr.firstAttribute == NSLayoutAttributeLeading || ctr.secondAttribute == NSLayoutAttributeLeading) {

                                            ctr.constant = 8.f;

                                        }

                                    }

                             applied = YES;

                    }

                    • Re: iOS 11 - UIBarButtonItem horizontal position
                      timurbeg86 Level 1 Level 1 (0 points)

                      Thanks, wang_stg.

                      Your solution worked for me.

                       

                      Here is swift version of the code: (SnapKit is used for defining constraints)

                       

                      import UIKit

                       

                      enum BarButtonPosition {

                          case left

                          case right

                      }

                      public class BarButton: UIButton {

                          var position: BarButtonPosition! = .left

                          private var applied: Bool = false

                       

                          override public func layoutSubviews() {

                              super.layoutSubviews()

                        

                              let os_version = OperatingSystemVersion(majorVersion: 11, minorVersion: 0, patchVersion: 0)

                              if applied || !ProcessInfo.processInfo.isOperatingSystemAtLeast(os_version) {

                                  return

                              }

                        

                              var view: UIView! = self

                              while (!view.isKind(of: UINavigationBar.self) && view.superview != nil) {

                                  if let stackView = view.superview {

                                      if (stackView.isKind(of: UIStackView.self) && stackView.superview != nil) {

                                          if self.position == .left {

                                              stackView.snp.makeConstraints({ (make) in

                                                  make.leading.equalToSuperview().offset(8)

                                              })

                                              applied = true

                                        

                                          } else if self.position == .right {

                                              stackView.snp.makeConstraints({ (make) in

                                                  make.trailing.equalToSuperview().offset(-8)

                                              })

                                              applied = true

                                          }

                                          break

                                      }

                                      view = stackView

                                  }

                              }

                          }

                      }

                    • Re: iOS 11 - UIBarButtonItem horizontal position
                      WATER1350 Level 1 Level 1 (0 points)

                      IOS 11 navigation bar use auto layout, so you should change the frame or position using NSLayoutConstraint

                      • Re: iOS 11 - UIBarButtonItem horizontal position
                        E.Otten_Trust Level 1 Level 1 (0 points)

                        Anyone found a solution for this issue? It makes sense they moved to UIStackView but I can't imagine why anyone would want a different offset for UIBarButtonItems that use custom views.

                         

                        While browsing through the UIStackView docs I've found multiple properties/methods that could help with aligning the buttonitems:

                        If only we could access the UIStackView that holds the left- and rightBarButtonItems...

                        • Re: iOS 11 - UIBarButtonItem horizontal position
                          vvlong Level 1 Level 1 (0 points)
                          
                          _button.translatesAutoresizingMaskIntoConstraints = NO;
                          

                          iOS 11 set this to YES by default, try to set this property, and adjust button:

                          
                          [_button setContentEdgeInsets:UIEdgeInsetsMake(10, 10, 10, 20)];
                          

                           

                          This may help you.

                          • Re: iOS 11 - UIBarButtonItem horizontal position
                            gmarmas Level 1 Level 1 (0 points)

                            Alright people, I think I got something.

                             

                            After two days of constant struggling with all kinds of workarounds and after diving into private view hierarchies, this is what I concluded to:

                             

                            Here's the view hierarchy of a UINavigationBar to its UIBarButtonItems:

                            UINavigationBar > _UINavigationBarContentView > _UIButtonBarStackView(s) > UIButton(s)

                             

                            By independently looking at each one of the views and their auto layout constraints (not by the way visible in the Debug View Hierarchy!), I could see that there are constraints to the layout guide, and that was the cause of the weird horizontal positioning of the UIBarButtonItems.

                             

                            In particular the content view's layout margins were the following (output from the console):

                             

                            (lldb) po self.navigationController?.navigationBar.subviews[2].layoutMargins
                            ▿ Optional<UIEdgeInsets>
                              ▿ some : UIEdgeInsets
                                - top : 0.0
                                - left : 16.0
                                - bottom : 0.0
                                - right : 16.0
                            
                            
                            
                            
                            
                            
                            
                            
                            
                            
                            
                            
                            
                            

                             

                            What I found is that simply setting the layout margins of the content view would fix the issue. You can do this easily in the layoutSubviews() method of your custom UINavigationBar. Since accessing the private view directly wouldn't be safe, iterating through all its subviews does the trick.

                             

                            override func layoutSubviews() {
                              super.layoutSubviews()
                            
                              for view in subviews {
                                view.layoutMargins = .zero
                              }
                            }
                            
                            
                            
                            

                             

                            Not the cleanest solution but I guess will do until Apple addresses the issue.

                              • Re: iOS 11 - UIBarButtonItem horizontal position
                                ddramowicz Level 1 Level 1 (0 points)

                                that is pretty useful. i was looking at the same thing today and it makes me wonder, why is this not possible by setting the layoutMargins of the navigationBar? since the layoutmargins of view of uiviewcontrollers is now customizable using

                                viewRespectsSystemMinimumLayoutMargins = false
                                
                                

                                doesn't it makes sense to allow this for uinavigationcontrollers? however, this approach has no effect. UIButtonBarStackView continue to use the system layout margins of the nav bar.

                                • Re: iOS 11 - UIBarButtonItem horizontal position
                                  john07 Level 1 Level 1 (0 points)

                                      override func layoutSubviews() {

                                          super.layoutSubviews();

                                          if #available(iOS 11, *){

                                              loop:

                                              for view in subviews {

                                                  for stack in view.subviews {

                                                      if stack is UIStackView {

                                                          stack.superview?.layoutMargins = .zero;

                                                          break loop;

                                                      }

                                                  }

                                              }

                                          }

                                      }

                                • Re: iOS 11 - UIBarButtonItem horizontal position
                                  make.icloud Level 1 Level 1 (0 points)

                                  Just a workaround for my case: I would like an imageView stick to the left edge of navigationBar.

                                   

                                              let logoImage = UIImage(named: "your_image")

                                              let logoImageView = UIImageView(image: logoImage)

                                              logoImageView.frame = CGRect(x: -16, y: 0, width: 150, height: 44)

                                              logoImageView.contentMode = .scaleAspectFit

                                              let logoView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))

                                              logoView.clipsToBounds = false

                                              logoView.addSubview(logoImageView)

                                              let logoItem = UIBarButtonItem(customView: logoView)

                                              navigationItem.leftBarButtonItem = logoItem

                                  • Re: iOS 11 - UIBarButtonItem horizontal position
                                    ekingo Level 1 Level 1 (0 points)

                                    It seems this is the only way, I guess navigationBar layout uses its own margins , so ignore the constraints set out of its own class.

                                     

                                    - (void)layoutSubviews {
                                        [super layoutSubviews];
                                       
                                        if (@available(iOS 11, *)) {
                                            self.layoutMargins = UIEdgeInsetsZero;
                                           
                                            for (UIView *subview in self.subviews) {
                                                if ([NSStringFromClass([subview class]) containsString:@"ContentView"]) {
                                                    UIEdgeInsets oEdges = subview.layoutMargins;
                                                    subview.layoutMargins = UIEdgeInsetsMake(0, 0, 0, oEdges.right);
                                                }
                                            }
                                        }
                                    }