做者:陈浩 贝聊科技移动开发部 iOS 工程师ios
本文已发表在我的博客。git
之前咱们经常使用 fixedSpace
的方式为 UINavigationBar 上的 UIBarButtonItem 设置间距。然而在 iOS 11 下 UIBarButtonItem
width 属性不但失效了,UIBarButtonItem
也开始使用 auto layout 布局,对此咱们须要设置 UIBarButtonItem
子 view 的约束。除此以外,苹果还修改了 UINavigationBar
的实现。直到 iOS 10 UINavigationBar
都是采用手动布局,全部的子 view 都是直接加在 UINavigationBar
上。可是,从 iOS 11 开始, UINavigationBar
使用了 auto layout 来布局它的内容子 view,而且子 view 加在了 _UINavigationBarContentView
上。github
iOS 11以前的作法:bash
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
target:nil
action:nil];
negativeSpacer.width = -8;
[self.navigationItem setLeftBarButtonItems:@[negativeSpacer, button] animated:NO];
复制代码
先来看看 iOS 11 下 UINavigationBar
的视图层级:ide
UINavigationBar
| _UIBarBackground
| | UIImageView
| | UIImageView
| _UINavigationBarLargeTitleView
| | UILabel
| _UINavigationBarContentView
| | _UIButtonBarStackView
| | | _UIButtonBarButton
| | | | _UIModernBarButton
| | | | | UIButtonLabel
| _UINavigationBarContentView
| | _UIButtonBarStackView
| | | _UITAMICAdaptorView // customView
| | | | UIBarButtonItem
| | | | | UIImageView
| | | | | UIButtonLabel
复制代码
经过 View Debug 工具可知,原来是 stackView 限制了 customView 的宽度以及引发了偏移:工具
contentView |<-fullWidth----------->|
stackView |<-stackViewWidth->|
customView |<-reducedWidth--->|
复制代码
在这次深挖以前,贝聊客户端的开发小哥们因为项目工期紧以及适配 iOS 11 工做量大,暂时是经过设置 UIButton
的 setContentEdgeInsets:
来实现的,这在当时看来是以最小的改动完成了适配,直到 iOS 11.2 这个版本的推出,咱们发现当侧滑返回时,导航条的返回按钮会被切掉一点角。(这个方法还有个小缺点是点击区域过小了)布局
碰巧的是,个人 leader 刚好发现了钉钉也有相似的问题。ui
iOS 11 虽然已经推出好几个月了,这个问题可能还在困扰着部分同行,接下来就讲讲贝聊是如何解决这个问题的。atom
因为你们知道 fixed space 失效是系统换成了 auto layout 来实现,因此网上的大部分文章也都是修改 constraint。遗憾的是,我谷歌到挺多使用这种方式去修改要获取到 UINavigationBar
的私有子 view,譬如 contentView
或 barStackView
,再为私有子 view 添加 leading 和 trailing 的约束等。spa
我并无尝试这种方法的可行性,由于始终以为获取私有子 view 的作法比较脆弱,苹果一旦更换实现,程序的健壮性很差保障。但能够肯定的是,解决这个问题的思路大体是修改约束,设法摆脱 stackView 的限制。
在 auto layout 中,约束是使用 alignment rectangle 来肯定视图的大小和位置。先看看 alignment rectangle 的做用是怎样,下图摘自《iOS Auto Layout Demystified》:
书中对此的说明是,假如设计师给了你张带角标的气泡图片,程序只指望对气泡进行居中,而图片的 frame 却包含了角标部分,这时能够 override alignmentRectForFrame:
、frameForAlignmentRect
。UIView
也给出了相对简便的属性 alignmentRectInsets
,须要注意的是,一旦设置了 alignmentRectInsets
,view 的 frame 就会根据 alignment rectangle 和 alignmentRectInsets
计算出来。
有了以上的概念后,贝聊的修复方法是子类化一个 UIBarButtonItem 的 customView:
@interface BLNavigationItemCustomView: UIView
@property (nonatomic) UIEdgeInsets alignmentRectInsetsOverride;
@end
@implementation BLNavigationItemCustomView
- (UIEdgeInsets)alignmentRectInsets {
if (UIEdgeInsetsEqualToEdgeInsets(self.alignmentRectInsetsOverride, UIEdgeInsetsZero)) {
return super.alignmentRectInsets;
} else {
return self.alignmentRectInsetsOverride;
}
}
@end
复制代码
再就是建立 customView 时针对 iOS 11 作特殊处理,以返回按钮为例:
if (@available(iOS 11.0, *)) {
button.alignmentRectInsetsOverride = UIEdgeInsetsMake(0, offset, 0, -(offset));
button.translatesAutoresizingMaskIntoConstraints = NO;
[button.widthAnchor constraintEqualToConstant:buttonWidth].active = YES;
[button.heightAnchor constraintEqualToConstant:44].active = YES;
}
复制代码
之因此设置 widthAnchor、heightAnchor 是前文提到的须要对 UIBarButtonItem
子 view 设置约束,我在实现时就遇到了怎么修改 frame 都没法撑大 customView 的状况,后来发现是没设置 widthAnchor。咱们接着用 View Debug 来看看实现的效果:
这儿有个问题就是 customView 有小部分超出了 stackView 的 bounds,致使了超出部分没法接收点击。有趣的是,使用 iOS 11 以前 fixed space 添加间距的作法能够减小 stackView 的 margin。
UIBarButtonItem *spacer = [UIBarButtonItem bl_barButtonItemSpacerWithWidth:-(offset)];
self.navigationItem.leftBarButtonItems = @[spacer, barButtonItem];
复制代码
结合上 fixed space 和 alignmentRectInsets,customView 将再也不超出它的父视图:
总之,咱们需继承复写 alignmentRectInsets
的 BLNavigationItemCustomView
,而后继续保持以前版本 fixed space 的处理,只针对 iOS 11 为 customView 新增约束,就可以使间距问题在新旧系统得以解决。
不客气的说,iOS 11 真的是一个挺难适配的版本,期间我都差点放弃对导航条间隔的适配了,好在最后仍是顺利解决了。若是你有更好的方式解决,欢迎赐教。