问题
iOS的导航栏,在iOS11中,针对左右bar button items的布局,发生了比较大的变化,这种变化通过比较视图层级的变化,可以更直观的体会到。
图1:iOS11之前

图2:iOS11

对比可知,在iOS11之前,按钮被直接加为UINavigationBar的子视图,布局的时候也没有使用约束。而在iOS11中,使用了一个_UINavigationBarContentView来作为容器,管理所有的按钮,并且在布局的时候使用了Stack View以及约束。
这个改变带来的一个后果是,之前我们设置按钮边距的代码会失效:
UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemFixedSpace target: nil action: nil];
fixedSpace.width = -8;
不管width设为任何负数值,在iOS11的系统上,你都会发现最左边或最右边的按钮,距离边缘的距离是20 points.
如何适配iOS11
在iOS11上,这个问题的根本原因是,_UINavigationBarContentView这个视图的layoutMargins属性的值为{0, 20, 0, 20},也就是在布局时,它的子视图无论如何都会距离最左边20 pts, 距离最右边也是20 pts。那么我们可以想办法修改成自己想要的值,比如8。下面给出一种采用runtime的方案。
void swizzleInstanceMethod(Class cls, SEL originSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(cls, originSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
if (class_addMethod(cls, originSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@interface UINavigationBar (WXBorderMargin)
@end
@implementation UINavigationBar (WXBorderMargin)
+ (void)load {
if (@available(iOS 11.0, *)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
swizzleInstanceMethod(self, @selector(layoutSubviews), @selector(wx_layoutSubviews));
});
}
}
- (void)wx_layoutSubviews {
[self wx_layoutSubviews];
for (UIView *view in self.subviews) {
for (UIView *stackView in view.subviews) {
if (@available(iOS 9.0, *)) {
if ([stackView isKindOfClass: [UIStackView class]]) {
stackView.superview.layoutMargins = UIEdgeInsetsMake(0, 8, 0, 8);
break;
}
}
}
}
}
@end
思路就是替换UINavigationBar的layoutSubviews方法,在方法内部,找到前面提到的那个容器视图,修改layoutMargins属性的值。
iOS11之前的系统
对于老系统,我们还是通过添加UIBarButtonSystemItemFixedSpace类型的UIBarButtonItem,来控制左右边距。如果不加的话,那么左右边距会有一个默认值,这个默认值,在比较宽的设备上,如plus, ipad上是20,其它设备上是16。如果要在所有的设备上实现统一的边距,那么需要设置的width的值是不同的。计算公式如下:
width = margin - DefaultSystemMargin
这里DefaultSystemMargin可能为16或是20。
Swift版本的解决方案
自定义一个UINavigationBar的子类:
public class WXNavigationBar: UINavigationBar {
public static let borderMarginForBarItem: CGFloat = 8
public override func layoutSubviews() {
if #available(iOS 11.0, *) {
let margin = type(of: self).borderMarginForBarItem
for view in subviews {
for stackView in view.subviews {
if stackView is UIStackView {
stackView.superview?.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
}
}
}
}
else {
super.layoutSubviews()
}
}
}
然后需要将系统的导航栏的类设置为刚才创建的子类,这里可以利用KVC,修改UINavigationController的navigationBar这个只读属性。代码如下:
//自定义的UINavigationController的子类的viewDidLoad方法
override func viewDidLoad() {
super.viewDidLoad()
let myNavigationBar = WXNavigationBar(frame: .zero)
setValue(myNavigationBar, forKey: "navigationBar")
}
网友评论