原文链接:UIBarButtonItem & iOS 11 by Kassem Wridan
发布时间:Oct 21, 2017
概述
iOS 11 和 Xcode 9已经正式发布1个月了,一个有趣的变化是UINavigationBar
中的UIBarButtonItem
的行为变化。
这里是我迄今为止的发现……
变化
总体来看,有这些要点:
- 自定义View
UIBarButtonItem
的点击区域变小了 - 返回按钮的点击区域也变小了
- 前一个页面标题更新时,当前页面的返回按钮的文本将不再自动更新
- 自定义View需要对Auto Layout友好
- 值为负的
UIBarButtonSystemItemFixedSpace
的UIBarButtonItem
已失效,并且现在最小值为8 - 覆写
alignmentRectInsets
可能导致自定义View超出点击范围。
下面的部分将深入说明以上各点。
点击区域
下面截图显示iOS 10系统中的截图,包含两个带有自定义View的UIBarButtonItem
。使用视图调试器检查视图可以看到自定义View的真实大小。
他们的点击区域实际上会更大一点,接近下图中的红色区域。
但是在iOS 11中,这个行为发生了改变。点击区域现在和自定义View的大小一致。
这个变化导致可用性变差,因为用户点击按钮变得困难了。要恢复到点击区域到更大的范围,自定义View需要在设置大小的时候加入额外间距。一个解决方法就是创建一个有最小尺寸的wrapper视图。
1 | class WrapperView: UIView { |
虽然和iOS 10上的方式不同,但也已经扩大了点击区域。
返回按钮
点击区域
如果标题很长,返回按钮上面的点击区域也会变得很小。在iOS 10上,返回按钮有比较大的最小尺寸和点击区域。
但是在iOS 11上,返回按钮的尺寸和点击区域变得更小。更糟糕的是当前页面的标题看起来像按钮的文本。
一个方式出现这个情况的做法是在上个页面手动设置一个backBarButtonItem
。
1 | navigationItem.backBarButtonItem = UIBarButtonItem(title: "First Screen", style: .plain, target: nil, action: nil) |
这个解决方法并不理想,因为:
- 如果我们能提前知道所有页面的标题,我们要在页面展示之前设置返回按钮。
- 如果我们不能提前知道所有页面的标题,我们要在任何地方设置返回按钮。
- 我们将丢失返回按钮的默认渲染行为,将不能根据可用空间自动适应尺寸。(苹果文档)
< First Screen
< Back
<
文本动态更新
iOS 11中的另一个变化是返回按钮的文本。例如,如果第一个页面的标题包含动态更新的数字。
在iOS 10上,如果标题更新了,下一个页面的返回按钮的文本也会更新。
但是在iOS 11上,返回按钮变成了静态的,不会动态更新。
解决这个问题的一个简单做法就是在第一个页面手动管理backBarButtonItem
。
1 | navigationItem.backBarButtonItem = UIBarButtonItem(title: "First (\(counter))", style: .plain, target: nil, action: nil) |
注意:每次都需要一个新的返回按钮实例,更新现有的实例并没有效果。
Auto Layout
UINavigationBar
现在在内部使用Auto Layout布局它的子View,同时也包含自定义View。今年早些时候,WWDC的Updating Your Apps for iOS 11演讲有提到这点。
在iOS11上检视导航栏显示按钮现在在内部通过栈视图(译者注:stack view)管理。
如果提供的自定义View正确实现了sizeThatFits
和intrinsicContentSize
方法,将不会有影响。
提醒一下,如果设置自定义View的translatesAutoresizingMaskIntoConstraints
为false
,将导致在iOS 10(如果你还支持这个版本)上出现约束冲突。这将导致自定义View被错误的放置到左上角。一个简单的解决方法是加上iOS 11的运行时可用性判断。
1 | let customView = createCustomView() |
自定义对齐
对于那些想精确控制自定义View对齐方式,特别是距离屏幕右边的间距,的人来说,在iOS 11以前,经常使用下面两个“trick”:
- 第一个是使用一个带有固定负值的空白项:
UIBarButtonItem(barButtonSystemItem: .fixedSpace …)
- 另一个是在自定义View上覆写
alignmentRectInsets
方法。
遗憾的是这两个行为在iOS 11上都变了。
固定空白项
使用自定义View的时候,默认间距是16pt
假如我们想减少到8pt,在iOS 10上,使用一个固定宽度为-8
的UIBarButtonItem
可以实现。
1 | let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) |
不幸的是,在iOS 11上这个方法失效了。
并且,从现象上来看任何小于8pt的值都会被忽略。这实际上是和在按钮项之间使用的固定宽度项的行为统一了。
对齐缩进
控制按钮对齐的第二个trick是覆写alignmentRectInsets
方法。
1 | class CustomView: UIView { |
设置UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 8)
这个值可以让上一个例子得到理想的对齐效果。
这个方法在iOS 11上依旧有效,但是有个小缺陷。仔细看一下,会发现最后一项的一小部分超出了栈视图的范围,那部分将收不到触摸事件,会导致那一项的点击区域变小!
变通方案
重新看一下上面的在iOS 11上的固定空白项的例子,会发现一个有意思的副作用,边距比通常值变小了。
是这样,这个现象在最后一个UIBarButtonItem
是非自定义View的时候会发生。
来看下这个例子,非自定义View的UIBarButtonItem
,到屏幕边缘的距离是8pt。
反之,当使用自定义View的时候,边距是16pt。
结合这些观察结果,我们在iOS 11上同样可以为自定义View实现8pt的边距。在末尾添加一个固定空白项可以使内部栈视图的边距减少,然后使用alignmentRectInsets
使自定义View偏移和空白项宽度相等的距离,这样可以使自定义视图和内部栈视图的末尾对齐。
1 | func alignedBarButtonItems() -> [UIBarButtonItem] { |
尽管这个变通方案有趣可行,但是它最多移动到8pt,移动更多会超出内部栈视图的边界。还有,这个方案依赖文档中没有提及的UIKit
的行为,这个行为很可能在未来会改变(就像定宽负值空白项一样)。
在开发者论坛中还有一些其他建议方案,可以让内部栈视图的边距消失——尽管他们需要自定义UINavigationBar
子类并修改内部行为(medeling with it’s internals),也不是很理想。
总结
谁能想到关于UIBarButtonItem
竟可以写这么多内容,我们就写了。如果发现问题或者其他优雅的解决方法,欢迎留言。