作者:陳浩 貝聊科技移動開發部 iOS 工程師
本文已發表在個人部落格。
以前我們常用 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
上。
iOS 11之前的做法:
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
target:nil
action:nil];
negativeSpacer.width = -8;
[self.navigationItem setLeftBarButtonItems:@[negativeSpacer, button] animated:NO];
複製程式碼
先來看看 iOS 11 下 UINavigationBar
的檢視層級:
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 恰好發現了釘釘也有類似的問題。
iOS 11 雖然已經推出好幾個月了,這個問題可能還在困擾著部分同行,接下來就講講貝聊是如何解決這個問題的。
由於大家知道 fixed space 失效是系統換成了 auto layout 來實現,所以網上的大部分文章也都是修改 constraint。遺憾的是,我谷歌到挺多使用這種方式去修改要獲取到 UINavigationBar
的私有子 view,譬如 contentView
或 barStackView
,再為私有子 view 新增 leading 和 trailing 的約束等。
我並沒有嘗試這種方法的可行性,因為始終覺得獲取私有子 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 真的是一個挺難適配的版本,期間我都差點放棄對導航條間隔的適配了,好在最後還是順利解決了。如果你有更好的方式解決,歡迎賜教。