作者:陳浩 貝聊科技移動(dòng)開發(fā)部 iOS 工程師
本文已發(fā)表在個(gè)人博客。
以前我們常用 fixedSpace 的方式為 UINavigationBar 上的 UIBarButtonItem 設(shè)置間距:
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
target:nil
action:nil];
negativeSpacer.width = -8;
[self.navigationItem setLeftBarButtonItems:@[negativeSpacer, button] animated:NO];
然而在 iOS 11 下 UIBarButtonItem width 屬性不但失效了,UIBarButtonItem 也開始使用 auto layout 布局,對(duì)此我們需要設(shè)置 UIBarButtonItem 子 view 的約束。除此之外,蘋果還修改了 UINavigationBar 的實(shí)現(xiàn)。直到 iOS 10 UINavigationBar 都是采用手動(dòng)布局,所有的子 view 都是直接加在 UINavigationBar 上。但是,從 iOS 11 開始, UINavigationBar 使用了 auto layout 來布局它的內(nèi)容子 view,并且子 view 加在了 _UINavigationBarContentView 上。
先來看看 iOS 11 下 UINavigationBar 的視圖層級(jí):
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--->|
在此次深挖之前,貝聊客戶端的開發(fā)小哥們由于項(xiàng)目工期緊以及適配 iOS 11 工作量大,暫時(shí)是通過設(shè)置 UIButton 的 setContentEdgeInsets: 來實(shí)現(xiàn)的,這在當(dāng)時(shí)看來是以最小的改動(dòng)完成了適配,直到 iOS 11.2 這個(gè)版本的推出,我們發(fā)現(xiàn)當(dāng)側(cè)滑返回時(shí),導(dǎo)航條的返回按鈕會(huì)被切掉一點(diǎn)角。(這個(gè)方法還有個(gè)小缺點(diǎn)是點(diǎn)擊區(qū)域太小了)

碰巧的是,我的 leader 恰好發(fā)現(xiàn)了釘釘也有類似的問題。
iOS 11 雖然已經(jīng)推出好幾個(gè)月了,這個(gè)問題可能還在困擾著部分同行,接下來就講講貝聊是如何解決這個(gè)問題的。
由于大家知道 fixed space 失效是系統(tǒng)換成了 auto layout 來實(shí)現(xiàn),所以網(wǎng)上的大部分文章也都是修改 constraint。遺憾的是,我谷歌到挺多使用這種方式去修改要獲取到 UINavigationBar 的私有子 view,譬如 contentView 或 barStackView,再為私有子 view 添加 leading 和 trailing 的約束等。
我并沒有嘗試這種方法的可行性,因?yàn)槭冀K覺得獲取私有子 view 的做法比較脆弱,蘋果一旦更換實(shí)現(xiàn),程序的健壯性不好保障。但可以確定的是,解決這個(gè)問題的思路大致是修改約束,設(shè)法擺脫 stackView 的限制。
在 auto layout 中,約束是使用 alignment rectangle 來確定視圖的大小和位置。先看看 alignment rectangle 的作用是怎樣,下圖摘自《iOS Auto Layout Demystified》:


書中對(duì)此的說明是,假如設(shè)計(jì)師給了你張帶角標(biāo)的氣泡圖片,程序只期望對(duì)氣泡進(jìn)行居中,而圖片的 frame 卻包含了角標(biāo)部分,這時(shí)可以 override alignmentRectForFrame:、frameForAlignmentRect。UIView 也給出了相對(duì)簡便的屬性 alignmentRectInsets,需要注意的是,一旦設(shè)置了 alignmentRectInsets,view 的 frame 就會(huì)根據(jù) alignment rectangle 和 alignmentRectInsets 計(jì)算出來。
有了以上的概念后,貝聊的修復(fù)方法是子類化一個(gè) 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
再就是創(chuàng)建 customView 時(shí)針對(duì) 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;
}
之所以設(shè)置 widthAnchor、heightAnchor 是前文提到的需要對(duì) UIBarButtonItem 子 view 設(shè)置約束,我在實(shí)現(xiàn)時(shí)就遇到了怎么修改 frame 都無法撐大 customView 的情況,后來發(fā)現(xiàn)是沒設(shè)置 widthAnchor。我們接著用 View Debug 來看看實(shí)現(xiàn)的效果:

這兒有個(gè)問題就是 customView 有小部分超出了 stackView 的 bounds,導(dǎo)致了超出部分無法接收點(diǎn)擊。有趣的是,使用 iOS 11 之前 fixed space 添加間距的做法可以減少 stackView 的 margin。
UIBarButtonItem *spacer = [UIBarButtonItem bl_barButtonItemSpacerWithWidth:-(offset)];
self.navigationItem.leftBarButtonItems = @[spacer, barButtonItem];
結(jié)合上 fixed space 和 alignmentRectInsets,customView 將不再超出它的父視圖:

總之,我們需繼承復(fù)寫 alignmentRectInsets 的 BLNavigationItemCustomView,然后繼續(xù)保持之前版本 fixed space 的處理,只針對(duì) iOS 11 為 customView 新增約束,就可使間距問題在新舊系統(tǒng)得以解決。
總結(jié)
不客氣的說,iOS 11 真的是一個(gè)挺難適配的版本,期間我都差點(diǎn)放棄對(duì)導(dǎo)航條間隔的適配了,好在最后還是順利解決了。如果你有更好的方式解決,歡迎賜教。