[貝聊科技]iOS 11 怎樣為導航條上的 UIBarButtonItem 設定間距

貝聊科技發表於2019-03-02

作者:陳浩  貝聊科技移動開發部  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 工作量大,暫時是通過設定 UIButtonsetContentEdgeInsets: 來實現的,這在當時看來是以最小的改動完成了適配,直到 iOS 11.2 這個版本的推出,我們發現當側滑返回時,導航條的返回按鈕會被切掉一點角。(這個方法還有個小缺點是點選區域太小了)

img1

碰巧的是,我的 leader 恰好發現了釘釘也有類似的問題。

[貝聊科技]iOS 11 怎樣為導航條上的 UIBarButtonItem 設定間距

iOS 11 雖然已經推出好幾個月了,這個問題可能還在困擾著部分同行,接下來就講講貝聊是如何解決這個問題的。

由於大家知道 fixed space 失效是系統換成了 auto layout 來實現,所以網上的大部分文章也都是修改 constraint。遺憾的是,我谷歌到挺多使用這種方式去修改要獲取到 UINavigationBar 的私有子 view,譬如 contentViewbarStackView,再為私有子 view 新增 leading 和 trailing 的約束等。

我並沒有嘗試這種方法的可行性,因為始終覺得獲取私有子 view 的做法比較脆弱,蘋果一旦更換實現,程式的健壯性不好保障。但可以確定的是,解決這個問題的思路大致是修改約束,設法擺脫 stackView 的限制。

在 auto layout 中,約束是使用 alignment rectangle 來確定檢視的大小和位置。先看看 alignment rectangle 的作用是怎樣,下圖摘自《iOS Auto Layout Demystified》:

img2

img3

書中對此的說明是,假如設計師給了你張帶角標的氣泡圖片,程式只期望對氣泡進行居中,而圖片的 frame 卻包含了角標部分,這時可以 override alignmentRectForFrame:frameForAlignmentRectUIView 也給出了相對簡便的屬性 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 來看看實現的效果:

img4

這兒有個問題就是 customView 有小部分超出了 stackView 的 bounds,導致了超出部分無法接收點選。有趣的是,使用 iOS 11 之前 fixed space 新增間距的做法可以減少 stackView 的 margin。

UIBarButtonItem *spacer = [UIBarButtonItem bl_barButtonItemSpacerWithWidth:-(offset)];
self.navigationItem.leftBarButtonItems = @[spacer, barButtonItem];
複製程式碼

結合上 fixed space 和 alignmentRectInsets,customView 將不再超出它的父檢視:

img5

總之,我們需繼承複寫 alignmentRectInsetsBLNavigationItemCustomView,然後繼續保持之前版本 fixed space 的處理,只針對 iOS 11 為 customView 新增約束,就可使間距問題在新舊系統得以解決。

總結

不客氣的說,iOS 11 真的是一個挺難適配的版本,期間我都差點放棄對導航條間隔的適配了,好在最後還是順利解決了。如果你有更好的方式解決,歡迎賜教。

相關文章