在 iOS 11 以后,Apple 在导航栏中启用了自动布局的相关特性,这使得导航栏的使用方式发生了一些变化,今天咱们着重说说导航栏中 UIBarButtonItem 在 iOS 11 中的几点变化。bash
在 WWDC 2018 的 Updating Your App for iOS 11中,咱们能够知道 UINavigationBar 开始支持 Auto Layout 了。app
这对于 UIBarButtonItem 来说,意味着什么呢?经过 view debug 工具咱们能够发现,全部的 item 会被一个内置的 stack view 所管理。ide
当 Custom View 正确的实现了 sizeThatFits 或者 intrinsicContentSize 时,UI 的展示将不会出现问题。工具
在 iOS 11 中,为了充分发挥 Auto Layout 特性,难免须要将 UIBarButtonItem 里 Custom View 的 translatesAutoresizingMaskIntoConstraints 属性设置为 no,这就可能会形成它在 iOS 11 如下的系统中发生布局错乱,所以咱们须要在相应的地方写上以下代码。布局
UIView *view = [UIView new];
if(@available(iOS 11, *)){
view.translatesAutoresizingMaskIntoConstraints = NO;
}
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:view];
self.navigationItem.rightBarButtonItem = item;
复制代码
咱们以 Custom View 的方式建立两个 UIBarButtonItem,经过 view debug 工具能够查看 Custom View 的真实大小ui
在 iOS 10 以前的版本中,它的点击区域如红色区域所示:this
对于这个问题,能够有两种解决方案:atom
一种方式是经过重写 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
方法来修改控件的点击区域,保证其范围控制在 44 * 44 pt 以上。例以下面的示例代码:spa
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
CGSize acturalSize = self.frame.size;
CGSize minimumSize = kBarButtonMinimumTapAreaSize; // 44 * 44 pt
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);
}
复制代码
另外一种方式是建立一个中间层视图,保证其视图的大小不小于 44 * 44 便可,例以下面的代码:debug
@interface WrapperView : UIView
@property (nonatomic, assign) CGSize minimumSize;
@property (nonatomic, strong) UIView *underlyingView;
@end
@implementation WrapperView
- (instancetype)initWithUnderlyingView:(UIView *)underlyingView {
self = [super initWithFrame:underlyingView.bounds];
if (self) {
_underlyingView = underlyingView;
_minimumSize = CGSizeMake(44.0f, 44.0f);
underlyingView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:underlyingView];
NSLayoutConstraint *leadingConstraint = [underlyingView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor];
NSLayoutConstraint *trailingConstraint = [underlyingView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor];
NSLayoutConstraint *topConstraint = [underlyingView.topAnchor constraintEqualToAnchor:self.topAnchor];
NSLayoutConstraint *bottomConstraint = [underlyingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor];
NSLayoutConstraint *heightConstraint = [self.heightAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.height];
NSLayoutConstraint *widthConstraint = [self.widthAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.width];
[NSLayoutConstraint activateConstraints:@[leadingConstraint, trailingConstraint, topConstraint, bottomConstraint, heightConstraint, widthConstraint]];
}
return self;
}
@end
复制代码
固然最简单的方法就是让你的图片变大一些,好比增大留白……
在 iOS 11 以前,咱们有两种方式来修改 item 与屏幕的间距
当咱们使用 Custom View 类型的 item 时,item 与屏幕的边距默认为 16 或者 20 pt(PS:当屏幕为5.5寸屏时,边距为 20 pt),下图以 16 pt 为例:
UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spacer.width = -8;
self.navigationItem.rightBarButtonItems = @[spacer, item1, item2];
复制代码
这个方案在 iOS 10 及如下的系统是有效的,效果以下图所示:
可是一样的代码在 iOS 11 上就是失效的……
仔细研究 iOS 11 中的视图布局能够发现,fixed space 类型的 item 在 Stack View 的 leading 和 trailling 时的行为与其在 item 之间的行为保持一致,它的最小宽度都为 8pt,这也就说明了设置负数为何会不生效了。
咱们能够重写某个视图控件的 alignmentRectInsets 属性,例以下面的代码:
- (UIEdgeInsets)alignmentRectInsets {
return self.position == right ? UIEdgeInsetsMake(0, -8, 0, 8) : UIEdgeInsetsMake(0, 8, 0, -8);
}
复制代码
当咱们设置 UIEdgeInsetsMake(0, -8, 0, 8) ,并将这段代码用到前面的例子时,你会看到这样的结果:
乍一看,感受这个方法彷佛解决了全部的问题,但仔细观察,你就会发现这个方案也存在不完美的地方,例如超出 stack view 的部分将没法响应点击事件
方案三和方案二比较类似,经过修改 XXXEdgeInsets 属性确实能让控件在视觉上达到预期,但因为 stack View 的存在,即便修改了 Custom View 的 hitTest 区域,也会存在没法响应点击事件的问题,因此这个方案也不是一个完美的解决方案:
虽然刚才提到的三种解决方案在 iOS 11 中都没法完美解决问题,但咱们仍是发现了一个有意思的现象
下图是使用宽度为负值,类型为 Fixed Space 的 item 时的视图布局,虽然 fixed space 的存在 让它在视觉上看起来离屏幕边距仍是有点远,但 item 控件自身与屏幕的边距确实变小了。
这样说可能让人有点摸不着头脑,咱们不妨来看看这中间的区别,下图是使用了非 customView 建立的 UIBarButtonItem,与屏幕的间距为 8 pt(当屏幕为 5.5 寸时,为 12 pt)
UIButton *customButton = [UIButton buttonWithType:UIButtonTypeCustom];
customButton.overrideAlignmentRectInsets = UIEdgeInsetsMake(0, x, 0, -x); // you should do this in your own custom class
customButton.translatesAutoresizingMaskIntoConstraints = NO;
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:customButton]
UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
positiveSeparator.width = 8;
self.navigationItem.leftBarButtonItems = @{positiveSeparator, item, ...}
复制代码
UIBarButtonItem 在 iOS 11 上的变化让很多开发者都头疼不已,咱们在 Apple Developer Forums上也能够看到很多开发者的解决方案,大致都是去对 NavigationBar 作修改,好比修改约束,本身实现一个导航栏基类等,但对于 咱们现有的工程来讲,这些方案的实现代价都太大了。
这篇文章提出的解决方案不须要对导航栏自己作任何修改,只须要在 UIBarButtonItem 上作一些处理便可,对比社区里现有的方案,该适配方案对历史包袱比较沉重的项目来讲是值得尝试的。