UIBarButtonItem 在 iOS 11 上的改變及應對方案

SketchK發表於2018-10-30

總覽

在 iOS 11 之後,Apple 在導航欄中啟用了自動佈局的相關特性,這使得導航欄的使用方式發生了一些變化,今天我們著重說說導航欄中 UIBarButtonItem 在 iOS 11 中的幾點變化。

主要變化

  • 檢視層級的變化
  • 點選區域的變化
  • 與螢幕間距的變化

檢視層級變化

表現形式

在 WWDC 2018 的 Updating Your App for iOS 11中,我們可以知道 UINavigationBar 開始支援 Auto Layout 了。

這對於 UIBarButtonItem 來講,意味著什麼呢?通過 view debug 工具我們可以發現,所有的 item 會被一個內建的 stack view 所管理。

UIBarButtonItem 在 iOS 11 上的改變及應對方案

當 Custom View 正確的實現了 sizeThatFits 或者 intrinsicContentSize 時,UI 的展現將不會出現問題。

注意事項

在 iOS 11 中,為了充分發揮 Auto Layout 特性,不免需要將 UIBarButtonItem 裡 Custom View 的 translatesAutoresizingMaskIntoConstraints 屬性設定為 no,這就可能會造成它在 iOS 11 以下的系統中發生佈局錯亂,因此我們需要在相應的地方寫上如下程式碼。

UIView *view = [UIView new];
if(@available(iOS 11, *)){
  view.translatesAutoresizingMaskIntoConstraints = NO;
}
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:view];
self.navigationItem.rightBarButtonItem = item;
複製程式碼

點選區域的變化

表現形式

我們以 Custom View 的方式建立兩個 UIBarButtonItem,通過 view debug 工具可以檢視 Custom View 的真實大小

UIBarButtonItem 在 iOS 11 上的改變及應對方案

在 iOS 10 之前的版本中,它的點選區域如紅色區域所示:

UIBarButtonItem 在 iOS 11 上的改變及應對方案
在 iOS 11 中,它的點選區域發生了改變,改變的規則就是點選區域與 Custom View 自身大小保持一致

UIBarButtonItem 在 iOS 11 上的改變及應對方案

解決方案

對於這個問題,可以有兩種解決方案:

一種方式是通過重寫 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 方法來修改控制元件的點選區域,保證其範圍控制在 44 * 44 pt 以上。例如下面的示例程式碼:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGSize acturalSize = self.frame.size;
    CGSize minimumSize = kBarButtonMinimumTapAreaSize; // 44 * 44 pt
    CGFloat verticalMargin = acturalSize.height - minimumSize.height >= 0 ? 0 : ((minimumSize.height - acturalSize.height ) / 2);
    CGFloat horizontalMargin = acturalSize.width - minimumSize.width >= 0 ? 0 : ((minimumSize.width - acturalSize.width ) / 2);
    CGRect newArea = CGRectMake(self.bounds.origin.x - horizontalMargin, self.bounds.origin.y - verticalMargin, self.bounds.size.width + 2 * horizontalMargin, self.bounds.size.height + 2 * verticalMargin);
    return CGRectContainsPoint(newArea, point);
}
複製程式碼

另一種方式是建立一箇中間層檢視,保證其檢視的大小不小於 44 * 44 即可,例如下面的程式碼:

@interface WrapperView : UIView
@property (nonatomic, assign) CGSize minimumSize;
@property (nonatomic, strong) UIView *underlyingView;
@end
@implementation WrapperView
- (instancetype)initWithUnderlyingView:(UIView *)underlyingView {
    self = [super initWithFrame:underlyingView.bounds];
    if (self) {
        _underlyingView = underlyingView;
        _minimumSize = CGSizeMake(44.0f, 44.0f);
        underlyingView.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:underlyingView];
        NSLayoutConstraint *leadingConstraint = [underlyingView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor];
        NSLayoutConstraint *trailingConstraint = [underlyingView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor];
        NSLayoutConstraint *topConstraint = [underlyingView.topAnchor constraintEqualToAnchor:self.topAnchor];
        NSLayoutConstraint *bottomConstraint = [underlyingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor];
        NSLayoutConstraint *heightConstraint = [self.heightAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.height];
        NSLayoutConstraint *widthConstraint = [self.widthAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.width];
        [NSLayoutConstraint activateConstraints:@[leadingConstraint, trailingConstraint, topConstraint, bottomConstraint, heightConstraint, widthConstraint]];
    }
    return self;
}
@end
複製程式碼

UIBarButtonItem 在 iOS 11 上的改變及應對方案

當然最簡單的方法就是讓你的圖片變大一些,比如增大留白……

與螢幕間距的變化

在 iOS 11 之前,我們有兩種方式來修改 item 與螢幕的間距

  • 設定一個寬頻為負值,型別為 Fixed Space 的 item
  • 重寫 Custom View 的 alignmentRectInsets 方法
  • 如果 Custom View 為 UIButton 型別,可以重寫其 contentEdgeInsets/imageEdgeInsets/titleEdgeInsets 並修改其 hitTest 區域

iOS 10 中的解決方案一:使用寬度為負值,型別為 Fixed Space 的 item

當我們使用 Custom View 型別的 item 時,item 與螢幕的邊距預設為 16 或者 20 pt(PS:當螢幕為5.5寸屏時,邊距為 20 pt),下圖以 16 pt 為例:

UIBarButtonItem 在 iOS 11 上的改變及應對方案
如果我們希望 item 與螢幕邊距為 8pt 時,我們的解決方案通常是這樣的:

UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spacer.width = -8;
self.navigationItem.rightBarButtonItems = @[spacer, item1, item2];
複製程式碼

這個方案在 iOS 10 及以下的系統是有效的,效果如下圖所示:

UIBarButtonItem 在 iOS 11 上的改變及應對方案

但是同樣的程式碼在 iOS 11 上就是失效的……

UIBarButtonItem 在 iOS 11 上的改變及應對方案

仔細研究 iOS 11 中的檢視佈局可以發現,fixed space 型別的 item 在 Stack View 的 leading 和 trailling 時的行為與其在 item 之間的行為保持一致,它的最小寬度都為 8pt,這也就說明了設定負數為什麼會不生效了。

UIBarButtonItem 在 iOS 11 上的改變及應對方案

iOS 10 中的解決方案二:重寫 Custom View 的 alignmentRectInsets 方法

我們可以重寫某個檢視控制元件的 alignmentRectInsets 屬性,例如下面的程式碼:

- (UIEdgeInsets)alignmentRectInsets {
    return self.position == right ? UIEdgeInsetsMake(0, -8, 0, 8) : UIEdgeInsetsMake(0, 8, 0, -8);
}
複製程式碼

當我們設定 UIEdgeInsetsMake(0, -8, 0, 8) ,並將這段程式碼用到前面的例子時,你會看到這樣的結果:

UIBarButtonItem 在 iOS 11 上的改變及應對方案

乍一看,感覺這個方法似乎解決了所有的問題,但仔細觀察,你就會發現這個方案也存在不完美的地方,例如超出 stack view 的部分將無法響應點選事件

UIBarButtonItem 在 iOS 11 上的改變及應對方案

iOS 10 中的解決方案三:利用 XXXEdgeInsets 屬性來修改

方案三和方案二比較相似,通過修改 XXXEdgeInsets 屬性確實能讓控制元件在視覺上達到預期,但由於 stack View 的存在,即使修改了 Custom View 的 hitTest 區域,也會存在無法響應點選事件的問題,所以這個方案也不是一個完美的解決方案:

UIBarButtonItem 在 iOS 11 上的改變及應對方案

解決方案

雖然剛才提到的三種解決方案在 iOS 11 中都無法完美解決問題,但我們還是發現了一個有意思的現象

下圖是使用寬度為負值,型別為 Fixed Space 的 item 時的檢視佈局,雖然 fixed space 的存在 讓它在視覺上看起來離螢幕邊距還是有點遠,但 item 控制元件自身與螢幕的邊距確實變小了。

UIBarButtonItem 在 iOS 11 上的改變及應對方案

這樣說可能讓人有點摸不著頭腦,我們不妨來看看這中間的區別,下圖是使用了非 customView 建立的 UIBarButtonItem,與螢幕的間距為 8 pt(當螢幕為 5.5 寸時,為 12 pt)

UIBarButtonItem 在 iOS 11 上的改變及應對方案
下圖是使用了 customView 建立的 UIBarButtonItem,與螢幕的間距為 16 pt(當螢幕為 5.5 寸時,為 20 pt)
UIBarButtonItem 在 iOS 11 上的改變及應對方案
正是基於以上的觀察,我們發現只要在 stack view 中新增一個 非 customView 建立的 UIBarButtonItem, 系統就會給我們減少 item 與螢幕間的距離,如果此時我們再用 alignmentRectInsets 提供一個視覺上的偏移,就可以完美解決當前的問題。

UIButton *customButton = [UIButton buttonWithType:UIButtonTypeCustom];
customButton.overrideAlignmentRectInsets = UIEdgeInsetsMake(0, x, 0, -x); // you should do this in your own custom class
customButton.translatesAutoresizingMaskIntoConstraints = NO;
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:customButton]
UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
positiveSeparator.width = 8;
self.navigationItem.leftBarButtonItems = @{positiveSeparator, item, ...}
複製程式碼

UIBarButtonItem 在 iOS 11 上的改變及應對方案
不過需要注意的是,這個方法能只能保證 item 距離螢幕的邊緣為 8 - 12 pt,如果你想讓 item 與螢幕的距離更近一些的話,就可能會出現其他的問題。

結論

UIBarButtonItem 在 iOS 11 上的變化讓不少開發者都頭疼不已,我們在 Apple Developer Forums上也可以看到不少開發者的解決方案,大體都是去對 NavigationBar 做修改,比如修改約束,自己實現一個導航欄基類等,但對於 我們現有的工程來說,這些方案的實現代價都太大了。

這篇文章提出的解決方案不需要對導航欄本身做任何修改,只需要在 UIBarButtonItem 上做一些處理即可,對比社群裡現有的方案,該適配方案對歷史包袱比較沉重的專案來說是值得嘗試的。

相關文章