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
。但我们需要重写UINavigationBar
的hitTest
方法,设置当点击导航栏之外时不响应,即可解决问题。
@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上导航栏的结构。
iOS11修改了导航栏的实现,在原本就很复杂的图层上又加了新的图层。而现在的自定义导航栏按钮都放到了_UIButtonBarStackView
中,我们从顶级视图到自定义左侧导航栏按钮依次打印。
我们发现_UIButtonBarStackView
的x坐标是16(5.5寸设备上是20pt,其余设备是16pt),由于父视图做了约束,所以该_UIButtonBarStackView
不能放在x = 0
的坐标点上,这就是导致偏移的原因,我们最终可以通过设置它的父视图_UINavigationBarContentView
的layoutMargin
属性,来消除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的大小。
方案二
以导航栏右侧自定义按钮为例,导航栏右侧按钮比左侧按钮复杂的多,可能是单个图片的按钮、文本按钮或者其他复杂的视图。同时有可能设置UIBarButtonItem
的enabled
属性,来控制是否可点击。
如果通过方案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上高度矮的话,可以修改代码来重新设置高度即可。针对于一些特殊的视图,可以根据具体需求来进行具体的修改。