26
2017
09

iOS11返回按钮和导航栏右按钮的完美适配

iOS11更新后,用Xcode9跑一下自己的项目,发现返回按钮不灵敏了,点击10次只有3-4次点中。这是因为iOS11系统在导航栏里面的布局和控件都变化了,导致图片按钮(UIBarButtonItem中仅放图片的item的简称)的很小,几乎点不到,文字按钮(UIBarButtonItem中仅放文字的item简称)还可以点到。

这里写图片描述

我试图用runtime去获取系统的返回item的子视图去重新布局,结果都是私有API,获取不到;然而之前加弹簧解决方法在iOS11上也是行不通的。

 UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];

所以我用了下面的自己想出来的解决方案。

下面我来说一下我自己的完美解决方案。需要添加的代码少于60行,改动的文件为一个。

第一、首先看一下我的项目添加返回按钮的位置
我是在BaseNavigationController里面push的时刻,获取被push的VC,然后给VC添加的返回按钮。整体代码如下

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
    if (self.viewControllers.count>0) {
        viewController.hidesBottomBarWhenPushed = YES;
        BaseViewController *vc = (BaseViewController *)viewController;
        if (vc.isHideBackItem == YES) {
            vc.navigationItem.hidesBackButton = YES;
        }else{
            vc.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithIcon:[vc backIconName]?[vc backIconName]:@"arrows_top" highIcon:@"" target:self action:@selector(back:)];
        }
        if ([viewController isKindOfClass:[NSClassFromString(@"LoginViewController") class]]) {
            for (UIViewController *aVC in self.viewControllers) {
                if ([aVC isKindOfClass:[NSClassFromString(@"LoginViewController") class]]) {
                    [self popToViewController:aVC animated:YES];
                    return;
                }
            }
        }

    }else{

    }
    [super pushViewController:viewController animated:animated];
}

为了方便,我提取出来最关键的代码来解释

    vc.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithIcon:[vc backIconName]?[vc backIconName]:@"arrows_top" highIcon:@"" target:self action:@selector(back:)];

UIBarButtonItem的itemWithIcon:highIcon:方法是我对UIBarButtonItem的category的方法,下面看UIBarButtonItem的category方法声明,这里主要声明了两个初始化方法,分别针对有图片和文字的,其中文字的无bug,暂时不适配,不需要讲解。

    @interface UIBarButtonItem (addition)
    + (UIBarButtonItem *)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action;

    + (UIBarButtonItem *)itemWithIcon:(NSString *)icon highIcon:(NSString *)highIcon target:(id)target action:(SEL)action;
    @end

前面讲的是我的项目结构,后面要讲的是思路和代码解决方案

思路:自定义一个BackView继承UIView,把需要的字控件返回按钮UIButton放到这个自定义的BackView中,最后把BackView传递给UIBarButtonItem的customView,这样无理是iOS几都是完美适配了。不需要重新自定义导航栏,自定义返回按钮等等。我们依然用系统的UIBarButtonItem,并且改动的代码量很小的情况下解决。

代码:(这部分可以直接copy到自己项目中)

UIBarButtonItem+addition.h中的代码

#import <UIKit/UIKit.h>

@interface BackView:UIView

@property(nonatomic,strong)UIButton *btn;

@end


@interface UIBarButtonItem (addition)
+ (UIBarButtonItem *)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action;

+ (UIBarButtonItem *)itemWithIcon:(NSString *)icon highIcon:(NSString *)highIcon target:(id)target action:(SEL)action;
@end


UIBarButtonItem+addition.m中的代码

@implementation BackView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}
-(void)layoutSubviews{
    [super layoutSubviews];
    UINavigationBar *navBar = nil;
    UIView *aView = self.superview;
    while (aView) {
        if ([aView isKindOfClass:[UINavigationBar class]]) {
            navBar = (UINavigationBar *)aView;
            break;
        }
        aView = aView.superview;
    }
    UINavigationItem * navItem =   (UINavigationItem *)navBar.items.lastObject;
    UIBarButtonItem *leftItem = navItem.leftBarButtonItem;
    UIBarButtonItem *rightItem = navItem.rightBarButtonItem;


    if (rightItem) {//右边按钮
        BackView *backView = rightItem.customView;
        if ([backView isKindOfClass:self.class]) {
            backView.btn.x = backView.width -backView.btn.width;
        }
    }
    if (leftItem) {//左边按钮
//        BackView *backView = leftItem.customView;

    }
}



@end

#import "UIBarButtonItem+addition.h"

@implementation UIBarButtonItem (addition)
+ (UIBarButtonItem *)itemWithIcon:(NSString *)icon highIcon:(NSString *)highIcon target:(id)target action:(SEL)action {
    BackView *customView = [[BackView alloc] initWithFrame:CGRectMake(0, 0, 80, 44)];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:target action:action];
    [customView addGestureRecognizer:tap];
    customView.btn = [UIButton buttonWithType:UIButtonTypeCustom];
    customView.btn.titleLabel.font = [UIFont systemFontOfSize:16.0];
    if (icon) {
        [customView.btn setBackgroundImage:[UIImage imageNamed:icon] forState:UIControlStateNormal];
    }
    if (highIcon) {
        [customView.btn setBackgroundImage:[UIImage imageNamed:highIcon] forState:UIControlStateHighlighted];
    }
    customView.btn.frame = CGRectMake(0, 0, customView.btn.currentBackgroundImage.size.width, customView.btn.currentBackgroundImage.size.height);
    customView.btn.centerY = customView.centerY;
    [customView.btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    [customView addSubview:customView.btn];
    return  [[UIBarButtonItem alloc] initWithCustomView:customView];
}

+ (UIBarButtonItem *)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action {
    UIButton *btn = [[UIButton alloc] init];
    [btn setTitle:title forState:UIControlStateNormal];
    btn.titleLabel.font = [UIFont systemFontOfSize:16.0];
    [btn setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [btn setTitleColor:kTintColor forState:UIControlStateNormal];
    [btn setTitleColor:kTintColor forState:UIControlStateHighlighted];
    [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    btn.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, -15);
    btn.frame = CGRectMake(0, 0, title.length * 18, 30);
    return  [[UIBarButtonItem alloc] initWithCustomView:btn];
}

@end

这里再解释一下,不然有人迷糊,会说为什么改了这么多代码,还说改动的代码量很小。

这个分类iOS11适配之前就有的,只是这个方法+ (UIBarButtonItem )itemWithIcon:(NSString )icon highIcon:(NSString *)highIcon target:(id)target action:(SEL)action 里面初始化的为UIButton而不是现在的BackView.

iOS11后我的改动为:在当前文件(因为只有UIBarButtonItem的分类使用这个BackView,所以没必要新建文件,这也省不少事情)新建一个BackView类,BackView类的总代码量很少,声明里面为3行

@interface BackView:UIView

@property(nonatomic,strong)UIButton *btn;

@end

实现文件里面有34行代码,加一起不到40行

@implementation BackView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}
-(void)layoutSubviews{
    [super layoutSubviews];
    UINavigationBar *navBar = nil;
    UIView *aView = self.superview;
    while (aView) {
        if ([aView isKindOfClass:[UINavigationBar class]]) {
            navBar = (UINavigationBar *)aView;
            break;
        }
        aView = aView.superview;
    }
    UINavigationItem * navItem =   (UINavigationItem *)navBar.items.lastObject;
    UIBarButtonItem *leftItem = navItem.leftBarButtonItem;
    UIBarButtonItem *rightItem = navItem.rightBarButtonItem;
    if (rightItem) {//右边按钮
        BackView *backView = rightItem.customView;
        if ([backView isKindOfClass:self.class]) {
            backView.btn.x = backView.width -backView.btn.width;
        }
    }
    if (leftItem) {//左边按钮
//        BackView *backView = leftItem.customView;
    }
}
@end

最后改动的就是把分类方法中的UIButton换为BackView中防止UIButton,这样就很巧妙的解决了iOS11返回按钮的问题,如果是BackView在右边有图片按钮,那么可能导致图标的位置很靠左边,所以我在layoutSubviews里面已经做了适配,如果是rightItem,那么我重新布局了BackView里面子控件btn的位置。左按钮用默认的就可以,不用处理。

   if (rightItem) {//右边按钮
        BackView *backView = rightItem.customView;
        if ([backView isKindOfClass:self.class]) {
            backView.btn.x = backView.width -backView.btn.width;
        }
    }
    if (leftItem) {//左边按钮
//        BackView *backView = leftItem.customView;
    }

这里顺便提一个小常识,初始化customView的时候,就是我们的BackView的时候,frame的size给多少,看我的代码给的是(80,44);

    BackView *customView = [[BackView alloc] initWithFrame:CGRectMake(0, 0, 80, 44)];

为什么是80,44呢?因为一般的customView的宽度系统都是给的80,所以这里和系统保持一致,然后44应该大家都知道,是导航栏64-状态栏20的结果。

总结,因为是封装的项目架构,适配只需要从底层的一个类文件处理就OK,我这里处理的是UIBarButtonItem+addition分类这一个文件,仅仅动了这个一个文件。一处改动,项目所有的左右按钮都适配到了。体现了封装的好处。

上一篇:React Native 解决RenderRow只渲染一次 下一篇:从属性动画看自定义View(1)