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

總覽

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

主要變化

  • 視圖層級的變化
  • 點擊區域的變化
  • 與屏幕間距的變化

視圖層級變化

表現形式

在 WWDC 2018 的 Updating Your App for iOS 11中,咱們能夠知道 UINavigationBar 開始支持 Auto Layout 了。app

這對於 UIBarButtonItem 來說,意味着什麼呢?經過 view debug 工具咱們能夠發現,全部的 item 會被一個內置的 stack view 所管理。ide

當 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 的真實大小ui

在 iOS 10 以前的版本中,它的點擊區域如紅色區域所示:this

在 iOS 11 中,它的點擊區域發生了改變,改變的規則就是點擊區域與 Custom View 自身大小保持一致

解決方案

對於這個問題,能夠有兩種解決方案:atom

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

- (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 便可,例以下面的代碼:debug

@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
複製代碼

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

與屏幕間距的變化

在 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 爲例:

若是咱們但願 item 與屏幕邊距爲 8pt 時,咱們的解決方案一般是這樣的:

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

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

可是一樣的代碼在 iOS 11 上就是失效的……

仔細研究 iOS 11 中的視圖佈局能夠發現,fixed space 類型的 item 在 Stack View 的 leading 和 trailling 時的行爲與其在 item 之間的行爲保持一致,它的最小寬度都爲 8pt,這也就說明了設置負數爲何會不生效了。

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) ,並將這段代碼用到前面的例子時,你會看到這樣的結果:

乍一看,感受這個方法彷佛解決了全部的問題,但仔細觀察,你就會發現這個方案也存在不完美的地方,例如超出 stack view 的部分將沒法響應點擊事件

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

方案三和方案二比較類似,經過修改 XXXEdgeInsets 屬性確實能讓控件在視覺上達到預期,但因爲 stack View 的存在,即便修改了 Custom View 的 hitTest 區域,也會存在沒法響應點擊事件的問題,因此這個方案也不是一個完美的解決方案:

解決方案

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

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

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

下圖是使用了 customView 建立的 UIBarButtonItem,與屏幕的間距爲 16 pt(當屏幕爲 5.5 寸時,爲 20 pt)
正是基於以上的觀察,咱們發現只要在 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, ...}
複製代碼

不過須要注意的是,這個方法能只能保證 item 距離屏幕的邊緣爲 8 - 12 pt,若是你想讓 item 與屏幕的距離更近一些的話,就可能會出現其餘的問題。

結論

UIBarButtonItem 在 iOS 11 上的變化讓很多開發者都頭疼不已,咱們在 Apple Developer Forums上也能夠看到很多開發者的解決方案,大致都是去對 NavigationBar 作修改,好比修改約束,本身實現一個導航欄基類等,但對於 咱們現有的工程來講,這些方案的實現代價都太大了。

這篇文章提出的解決方案不須要對導航欄自己作任何修改,只須要在 UIBarButtonItem 上作一些處理便可,對比社區裏現有的方案,該適配方案對歷史包袱比較沉重的項目來講是值得嘗試的。

相關文章
相關標籤/搜索