iOS導航欄自定義按鈕響應區域優化

iOS 11中,系統重構了導航欄,UINavigationBar的層次結構發生了變化,同時影響了按鈕UINavigationItem的佈局位置以及響應區域。而針對於不同的系統,我們很多時候可能都需要做導航欄按鈕的響應區域的優化。
本文會針對兩個case來做導航欄響應區域的優化。

case 1:iOS11以下系統導航欄按鈕響應區域過大

該case是在相冊選擇頁面,導航欄右上角有一個取消按鈕,而視圖展示的是當前相冊中的圖片以及每個圖片右上角有一個可選擇的按鈕。

還在路上,稍等...

我們是通過[[UIBarButtonItem alloc] initWithCustomView:button]的方式來生成一個UIBarButtonItem對象的,我們可以看到按鈕的實際大小是綠色區域部分,而它的點擊響應範圍則是紅色框中的任意位置。這樣當我們點擊第一行最後一個照片的選擇按鈕時,會觸發點擊取消的操作,導致無法正常選擇。

通常方案

要解決這個問題大多數方案都是把UIButton放到一個UIView中,設置View的clipsToBounds以及userInteractionEnabled屬性,即可實現縮小點擊區域,代碼如下

UIButton *button = [[UIButton alloc] init];
[button setTitle:@"取消" forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:15.0f];
[button sizeToFit];
[button addTarget:self action:@selector(cancelBtnClicked) forControlEvents:UIControlEventTouchUpInside];
UIView * view = [[UIView alloc] initWithFrame:button.frame];
view.userInteractionEnabled = YES;
view.clipsToBounds = YES;
[view addSubview:button];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];

這樣修改後,按鈕的響應區域變成了這個樣子。

還在路上,稍等...

我們發現,當前的響應區域不會覆蓋到圖片的選擇按鈕了,但是還是超過了導航欄本身,在小分辨率手機上依然存在誤觸的情況,同時在導航欄上的相應區域也變小了,這樣可能會導致取消按鈕本身的點擊響應變得不靈敏。

更好的方式

我們繼續使用UIButton來初始化一個UIBarButtonItem,不需要在外層嵌套一個UIView。但我們需要重寫UINavigationBarhitTest方法,設置當點擊導航欄之外時不響應,即可解決問題。

@implementation UINavigationBar (TGL)

+ (void)load
{
    [UINavigationBar swizzleInstanceMethod:@selector(hitTest:withEvent:) withMethod:@selector(tgl_hitTest:withEvent:)];
}

- (UIView *)tgl_hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event])
        self.userInteractionEnabled = YES;
    else
        self.userInteractionEnabled = NO;
    return [self tgl_hitTest:point withEvent:event];
}

@end
還在路上,稍等...

現在看來,取消按鈕的響應區域是我們期望的。

PS:iOS11及以上系統不需要這樣設置

case 2:iOS11及以上系統導航欄按鈕響應區域過小

還在路上,稍等...

iOS11系統上導航欄的最左側和最右側有一定的區域無法響應點擊事件。以左側自定義返回按鈕爲例,可以有很多方法在視覺上讓導航欄按鈕向左側偏移。但基本上,無論如何設置上圖顯示的紅色區域,都無法響應點擊事件。而一些老用戶,之前習慣點擊最左邊,那麼就可能會讓他們覺得點擊不靈敏。

問題分析

我們來看下iOS11上導航欄的結構。

image

iOS11修改了導航欄的實現,在原本就很複雜的圖層上又加了新的圖層。而現在的自定義導航欄按鈕都放到了_UIButtonBarStackView中,我們從頂級視圖到自定義左側導航欄按鈕依次打印。

image

我們發現_UIButtonBarStackView的x座標是16(5.5寸設備上是20pt,其餘設備是16pt),由於父視圖做了約束,所以該_UIButtonBarStackView不能放在x = 0的座標點上,這就是導致偏移的原因,我們最終可以通過設置它的父視圖_UINavigationBarContentViewlayoutMargin屬性,來消除16pt的偏移。

@implementation UINavigationItem (TGL)

+ (void)load
{
    //只需要修復iOS11及以上的系統,暫時測iOS12beta版本同樣有效
    if (kiOS11Later)
        [UINavigationBar swizzleInstanceMethod:@selector(layoutSubviews) withMethod:@selector(tgl_layoutSubviews)];
}

- (void)tgl_layoutSubviews
{
    [self tgl_layoutSubviews];
    
    for (UIView * subview in self.subviews)
    {
        if ([NSStringFromClass(subview.class) containsString:@"ContentView"])
        {
            //這樣設置,左右側的偏移就沒有了
            //當然如果有其他需求,可以設置成其他參數
            if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
                UIEdgeInsets margins = subview.layoutMargins;
                subview.frame = CGRectMake(-margins.left, -margins.top, margins.left + margins.right + subview.frame.size.width, margins.top + margins.bottom + subview.frame.size.height);
            } else {
                subview.layoutMargins = UIEdgeInsetsZero;
            }
        }
    }
}

@end

這樣之後,我們就可以直接設置UIBarButtonItem而不需要對它的customView再次設置偏移等等,先看下效果。

還在路上,稍等...

如圖所示,左右邊距確實沒有了,但是如果這時,我們還希望導航欄按鈕的展示效果與原來的效果一致或者與iOS11以下的系統表現一致該如何,接下來我們介紹兩種方法來修改這個問題。

方法一

以導航欄左側自定義返回按鈕爲例,一般左側只會有一個返回按鈕,不會承載更多功能。我們期望可以得到一個響應範圍較爲合適的區域,例如我們把button的大小設置爲(40, 40),而我們的返回按鈕圖片只有15pt*15pt,那麼我們直接生成後,效果圖片居中。

//  設置導航欄按鈕代碼
UIButton * button = [TGLPictureViewMaker makeButtonWithImageName:imageName andHighLightedImageName:highlightedImageName];
CGSize imageSize = button.imageView.size;
button.size = CGSizeMake(40, 40);
button.imageView.size = imageSize;
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
還在路上,稍等...

我們發現在iOS11上完美,但是iOS11級以下的設備,左側有一個似乎很熟悉的間隔,沒錯這個間距就是之前說的16pt(或者5.5寸設備20pt),但是不需要慌,我們知道可以在左側增加一個UIBarButtonSystemItemFixedSpace類型的UIBarButtonItem來佔位,爲了不影響已有功能,我們可以通過MethodSwizzle來實現。

static CGFloat kDefaultSpaceBeforeiOS11;

@implementation UINavigationItem (TGL)

+ (void)load
{
    [self swizzleInstanceMethod:@selector(setLeftBarButtonItem:) withMethod:@selector(tgl_setLeftBarButtonItem:)];
    [self swizzleInstanceMethod:@selector(setLeftBarButtonItems:) withMethod:@selector(tgl_setLeftBarButtonItems:)];
    
    [self swizzleInstanceMethod:@selector(setRightBarButtonItem:) withMethod:@selector(tgl_setRightBarButtonItem:)];
    [self swizzleInstanceMethod:@selector(setRightBarButtonItems:) withMethod:@selector(tgl_setRightBarButtonItems:)];
    
    kDefaultSpaceBeforeiOS11 = kAPPWidth > 375 ? -20 : -16;
}

- (void)tgl_setLeftBarButtonItem:(UIBarButtonItem *)barButtonItem
{
    //  如果是iOS11及以後或者barButtonItem爲nil,我們不需要做其他處理,交給原來的函數處理就好了
    if (kiOS11Later || barButtonItem == nil)
        [self tgl_setLeftBarButtonItem:barButtonItem];
    else
        [self setLeftBarButtonItems:@[barButtonItem]];
}

- (void)tgl_setLeftBarButtonItems:(NSArray <UIBarButtonItem *> *)barButtonItems
{
    if (kiOS11Later || barButtonItems == nil)
        [self tgl_setLeftBarButtonItems:barButtonItems];
    else
    {
        if (barButtonItems.count)
        {
            //解決iOS11之前的偏移
            NSMutableArray * items = [NSMutableArray arrayWithObject:[self fixedSpaceWithWidth:kDefaultSpaceBeforeiOS11]];
            [items addObjectsFromArray:barButtonItems];
            [self tgl_setLeftBarButtonItems:items];
        }
        else
            [self tgl_setLeftBarButtonItems:barButtonItems];
    }
}

- (UIBarButtonItem *)fixedSpaceWithWidth:(CGFloat)width
{
    UIBarButtonItem * fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                                                                                target:nil
                                                                                action:nil];
    fixedSpace.width = width;
    return fixedSpace;
}

@end

這樣我們就解決了在iOS11以下設備上的對於左側導航欄的兼容,看下效果

還在路上,稍等...

上面圖中的紅框是兩個系統分別對應的按鈕的點擊事件,雖然iOS11上沒有額外的相應區域,但是點擊起來也是完全沒有問題,如果需要在iOS11上擴展點擊事件,請重寫HitTest方法或者擴大button的大小。

方案二

以導航欄右側自定義按鈕爲例,導航欄右側按鈕比左側按鈕複雜的多,可能是單個圖片的按鈕、文本按鈕或者其他複雜的視圖。同時有可能設置UIBarButtonItemenabled屬性,來控制是否可點擊。

如果通過方案1的方法來設置右側導航欄,在iOS11以下設備,當您使用self.navigationItem.rightBarButtonItem方法設置導航欄按鈕時,在運行時,會被替換成setRightBarButtonItems,而在之後的控制器代碼中設置self.navigationItem.rightBarButtonItem.enabled=NO時,則無效。

還在路上,稍等...

相同的代碼,在iOS11上不可點擊表現正常,但是在iOS10上表現的不正常。同時右側導航欄按鈕不是圖片了,是文字了,那麼我們可以通過在iOS11上修改viewFrame來實現。

@implementation UINavigationItem (TGL)

+ (void)load
{
    [self swizzleInstanceMethod:@selector(setRightBarButtonItem:) withMethod:@selector(tgl_setRightBarButtonItem:)];
    [self swizzleInstanceMethod:@selector(setRightBarButtonItems:) withMethod:@selector(tgl_setRightBarButtonItems:)];
    
    kDefaultNavigationItemSpace = kAPPWidth > 375 ? 20 : 16;
}

- (void)tgl_setRightBarButtonItem:(UIBarButtonItem *)barButtonItem
{
    if (kiOS11Later)
    {
        //給view增加2倍的默認偏移的寬度,這樣位置正好可以與iOS11以下設備的位置一致,而且相應區域較大
        UIView * view = barButtonItem.customView;
        if ([view isKindOfClass:[UIButton class]])
            barButtonItem.customView.width += kDefaultNavigationItemSpace * 2;
        //這裏針對其他類型的視圖可以做其他處理
        //...
    }

    [self tgl_setRightBarButtonItem:barButtonItem];
}

//目前app內沒有設置多個導航欄右側按鈕,暫時沒有測試效果
- (void)tgl_setRightBarButtonItems:(NSArray <UIBarButtonItem *> *)barButtonItems
{
    if (kiOS11Later)
    {
        for (UIBarButtonItem * barButtonItem in barButtonItems)
            barButtonItem.customView.width += kDefaultNavigationItemSpace * 2;
    }
    [self tgl_setRightBarButtonItems:barButtonItems];
}

@end

看下效果

還在路上,稍等...

可以看到現在在不同系統上按鈕位置一致,而響應區域也基本是比較容易點擊的,如果嫌棄iOS11上高度矮的話,可以修改代碼來重新設置高度即可。針對於一些特殊的視圖,可以根據具體需求來進行具體的修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章