iOS 11 UINavigationBar 条形按钮项目对齐

iOS 11 UINavigationBar bar button items alignment

我正在将我的应用程序升级到 iOS 11,我发现导航栏有一些问题,我的部分问题已经在这里提问了,所以我不会在这个问题中提及它们。

所讨论的特定问题是导航栏的栏按钮项的间距不同。我的主要左右栏按钮项目现在更靠近屏幕的水平中心,我无法将它们移动到屏幕边缘附近。过去,我使用了自定义 UIButton 子类并创建了带有自定义视图的条形按钮项。对齐解决方案是 alignmentRectInsetscontentEdgeInsets,现在我无法使用这种方法产生预期的结果。

编辑:
我已经用 iOS 11 beta 2 重新测试,问题仍然存在。

编辑 2: 我已经用 iOS beta 3 重新测试,问题仍然存在。

我注意到了类似的问题。

我报告了一个 Apple Radar 我们注意到的类似问题,#32674764 如果你想参考它,如果你创建一个 Radar。

我还在 Apple 的 论坛 中创建了一个主题,但还没有反馈: https://forums.developer.apple.com/message/234654

解决方案一: 鉴于 Apple 回应这是预期行为,我们通过删除工具栏并添加自定义视图解决了该问题。

我意识到这可能并非在所有情况下都可行,但常规 UIView 比 Apple 控制按钮位置的工具栏和导航栏更容易定制应用程序的外观运行ce .

我们没有将自定义按钮设置为 ui 栏按钮 object 的自定义视图,而是将其设置为自定义中空白 ui 按钮的子视图查看。

通过这样做,我们能够 return 获得与 ios 10 应用程序相同的外观

方案二: 有点乱,我们将自定义视图按钮包装在外部 UIButton 中,以便可以设置框架位置。不幸的是,这确实使按钮的外左边缘 un-tappable 变了,但纠正了按钮位置的外观。参见示例:

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;

使用该方法,我们保留了导航栏并可以将按钮定位到需要的位置。

编辑:我们发现解决方案 2 不适用于 ios10,这可能只会影响一小部分被迫向后兼容的开发人员。

解决方案 3

我们真正更关心按钮向内拥挤的事实是导航栏的标题 运行 进入自定义左按钮,边距的大小不太重要并且使用作为制作 space 的工具。解决方法是简单地在左边的项目中添加一个 spacer 项目。

UIBarButtonItem* backButton = [[UIBarButtonItem alloc] initWithCustomView:button];
UIBarButtonItem* spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
controller.navigationItem.leftBarButtonItems = [NSArray arrayWithObjects:backButton, spacer, nil];

摘自:BarButtons 现在使用自动布局,因此需要约束。

if #available(iOS 9.0, *) {
  cButton.widthAnchor.constraint(equalToConstant: customViewButton.width).isActive = true
  cButton.heightAnchor.constraint(equalToConstant: customViewButton.height).isActive = true
}

Objective C

if (@available(iOS 9, *)) {
  [cButton.widthAnchor constraintEqualToConstant: standardButtonSize.width].active = YES;
  [cButton.heightAnchor constraintEqualToConstant: standardButtonSize.height].active = YES;
}

现在在 iOS11 中,您可以管理 UINavigationBarContentView 来调整左右约束,并管理 UIStackView 来调整按钮(或其他元素)。

这是我的导航栏解决方案,左侧和右侧都有项目。如果一侧有多个按钮,它也会修复。

- (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;
                        }
                    }
                }
            }
        }
    }
}


如您所见,没有必要像其他人那样添加更多约束。已经定义了约束,因此可以更改它们。

大约两天后,这是我能想到的最简单、最安全的解决方案。此解决方案仅适用于自定义视图栏按钮项目,其代码已包含在内。重要的是要注意导航栏上的左右边距没有从 iOS10 更改为 iOS11 - 它们仍然是 16px。这么大的边距很难有足够大的点击区域。

条形按钮现在使用 UIStackView 进行布局。将这些按钮移近边缘的先前方法涉及使用这些堆栈视图无法处理的负固定空间。

子类 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

使用 UINavigationController

#import "FWNavigationBar.h"

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

创建一个 UIBarButton

将此代码放在 UIBarButton 类别或您计划使用栏按钮的文件中,这将 return 使用 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];
}

在控制器中设置栏按钮

- (void)viewDidLoad {
    [super viewDidLoad];

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

最后,我建议将左右边距保持为零,并只调整按钮在图像中的位置。这将允许您利用整个可点击区域直至屏幕边缘。图片的高度也是如此 - 确保高度为 44 点。

Swift developer3214 的自定义 UINavigationBar 子类的 layoutSubviews 方法的版本():

override func layoutSubviews() {
    super.layoutSubviews();
    if #available(iOS 11, *){
        self.layoutMargins = UIEdgeInsets()
        for subview in self.subviews {
            if String(describing: subview.classForCoder).contains("ContentView") {
                let oldEdges = subview.layoutMargins
                subview.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: oldEdges.right)
            }
        }
    }
}

有一篇关于此的好文章:http://www.matrixprojects.net/p/uibarbuttonitem-ios11/

使用这个我们至少可以将 rightbarbuttonitems 向右推,直到它从 UINavigationBar 的尾部留下 8 像素的边距。

解释的真好。

我偶然发现了这个:UINavigationItem-Margin。 它就像一个魅力。

我遇到了同样的问题。我有三个按钮作为导航栏堆栈视图上的右栏按钮项目。所以,我更新了导航栏子视图的插图。

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

这里的 11 是我需要的 space。根据您的要求,它可以是任何东西。 此外,如果您想要带有 0 个插图的按钮,请将 view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11) 替换为 view.layoutMargins = UIEdgeInsets.zero

Another answer 可能会有帮助。

我的解决方案:它适用于 ios 9 - 12。您应该在函数 viewDidAppear(_ animated: Bool)viewDidLayoutSubviews()fixNavigationItemsMargin(margin:) 会修改 UINavigationController 堆栈。

你可以在 BaseNavigationController 中调用 fixNavigationItemsMargin(margin:) ,做一些共同的工作。并在UIViewController中调用fixNavigationItemsMargin(margin:)做精确布局

// 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({ [=10=].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({ [=10=].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
            }
        }
    }
}
}