[貝聊科技]iOS 11 怎樣為導(dǎo)航條上的 UIBarButtonItem 設(shè)置間距

作者:陳浩  貝聊科技移動(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è)置 UIButtonsetContentEdgeInsets: 來實(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ū)域太小了)

img1
img1

碰巧的是,我的 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,譬如 contentViewbarStackView,再為私有子 view 添加 leading 和 trailing 的約束等。

我并沒有嘗試這種方法的可行性,因?yàn)槭冀K覺得獲取私有子 view 的做法比較脆弱,蘋果一旦更換實(shí)現(xiàn),程序的健壯性不好保障。但可以確定的是,解決這個(gè)問題的思路大致是修改約束,設(shè)法擺脫 stackView 的限制。

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

img2
img2
img3
img3

書中對(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)的效果:

img4
img4

這兒有個(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 將不再超出它的父視圖:

img5
img5

總之,我們需繼承復(fù)寫 alignmentRectInsetsBLNavigationItemCustomView,然后繼續(xù)保持之前版本 fixed space 的處理,只針對(duì) iOS 11 為 customView 新增約束,就可使間距問題在新舊系統(tǒng)得以解決。

總結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容