简介
UIBarButtonItem 是一个 UIBarItem,是一种专门放在bar上的特殊button,UIBarItem是一个NSObject
风格
UIBarButtonSystemItemAction的风格,这是系统自带的按钮风格,看下图,你不用一个个试验,你也知道想用那个item,如下图:
常见几种简单用法一:
// 系统自带的符号
1.UIBarButtonItem *leftButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(selectLeftAction:)];
// 自定义文字---backBarButtonItem 右边"Back"按钮
2. self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc]initWithTitle:@"返回" style:UIBarButtonItemStyleDone target:nil action:nil];
// 设置所有UINavigationBar上文本的属性
NSDictionary *attributes =@{NSFontAttributeName: [UIFont fontWithName:@"Futura"size:18],
NSForegroundColorAttributeName: [UIColor redColor]};
[[UINavigationBar appearance]setTitleTextAttributes:attributes];
项目中常见用法二:
#import
typedef void (^TLBarButtonActionBlock)();
@interface UIBarButtonItem (Action)
- (id)initWithTitle:(NSString *)title actionBlick:(TLBarButtonActionBlock)actionBlock;
- (id)initWithImage:(UIImage *)image actionBlick:(TLBarButtonActionBlock)actionBlock;
@end
#import "UIBarButtonItem+Action.h"
#import
char * const UIBarButtonItemActionBlock = "UIBarButtonItemActionBlock";
@implementation UIBarButtonItem (Action)
- (id)initWithTitle:(NSString *)title actionBlick:(TLBarButtonActionBlock)actionBlock
{
if (self = [self initWithTitle:title style:UIBarButtonItemStylePlain target:nil action:nil]) {
[self setActionBlock:actionBlock];
}
return self;
}
- (id)initWithImage:(UIImage *)image actionBlick:(TLBarButtonActionBlock)actionBlock
{
if (self = [self initWithImage:image style:UIBarButtonItemStylePlain target:nil action:nil]) {
[self setActionBlock:actionBlock];
}
return self;
}
- (void)performActionBlock {
dispatch_block_t block = self.actionBlock;
if (block) {
block();
}
}
- (TLBarButtonActionBlock)actionBlock {
return objc_getAssociatedObject(self, UIBarButtonItemActionBlock);
}
- (void)setActionBlock:(TLBarButtonActionBlock)actionBlock {
if (actionBlock != self.actionBlock) {
[self willChangeValueForKey:@"actionBlock"];
objc_setAssociatedObject(self, UIBarButtonItemActionBlock, actionBlock, OBJC_ASSOCIATION_COPY);
[self setTarget:self];
[self setAction:@selector(performActionBlock)];
[self didChangeValueForKey:@"actionBlock"];
}
}
@end
其他用法:
- 可以参考QMUI对此进行红点设置
- 可以参考RAC对此进行事件的传递
UIBarButtonItem 在 iOS 11 上的改变及应对方案:
在 WWDC 2018 的 Updating Your App for iOS 11中,我们可以知道 UINavigationBar 开始支持 Auto Layout 了。
这对于 UIBarButtonItem 来讲,意味着什么呢?通过 view debug 工具我们可以发现,所有的 item 会被一个内置的 stack view
所管理。
当 Custom View 正确的实现了
sizeThatFits
或者intrinsicContentSize
时,UI 的展现将不会出现问题。
注意事项
在 iOS 11 中,为了充分发挥 Auto Layout 特性,不免需要将 UIBarButtonItem 里 Custom View 的 translatesAutoresizingMaskIntoConstraints 属性设置为 no,这就可能会造成它在 iOS 11 以下的系统中发生布局错乱,因此我们需要在相应的地方写上如下代码。
UIView *view = [UIView new];
if(@available(iOS 11, *)){
view.translatesAutoresizingMaskIntoConstraints = NO;
}
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:view];
self.navigationItem.rightBarButtonItem = item;
点击区域的变化
表现形式
我们以 Custom View 的方式创建两个 UIBarButtonItem,通过 view debug 工具可以查看 Custom View 的真实大小
在 iOS 10 之前的版本中,它的点击区域如红色区域所示:
在 iOS 11 中,它的点击区域发生了改变,改变的规则就是点击区域与 Custom View 自身大小保持一致
解决方案
对于这个问题,可以有两种解决方案:
一种方式是通过重写 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
方法来修改控件的点击区域,保证其范围控制在 44 * 44 pt 以上。例如下面的示例代码:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
CGSize acturalSize = self.frame.size;
CGSize minimumSize = kBarButtonMinimumTapAreaSize; // 44 * 44 pt
CGFloat verticalMargin = acturalSize.height - minimumSize.height >= 0 ? 0 : ((minimumSize.height - acturalSize.height ) / 2);
CGFloat horizontalMargin = acturalSize.width - minimumSize.width >= 0 ? 0 : ((minimumSize.width - acturalSize.width ) / 2);
CGRect newArea = CGRectMake(self.bounds.origin.x - horizontalMargin, self.bounds.origin.y - verticalMargin, self.bounds.size.width + 2 * horizontalMargin, self.bounds.size.height + 2 * verticalMargin);
return CGRectContainsPoint(newArea, point);
}
另一种方式是创建一个中间层视图,保证其视图的大小不小于 44 * 44 即可,例如下面的代码:
@interface WrapperView : UIView
@property (nonatomic, assign) CGSize minimumSize;
@property (nonatomic, strong) UIView *underlyingView;
@end
@implementation WrapperView
- (instancetype)initWithUnderlyingView:(UIView *)underlyingView {
self = [super initWithFrame:underlyingView.bounds];
if (self) {
_underlyingView = underlyingView;
_minimumSize = CGSizeMake(44.0f, 44.0f);
underlyingView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:underlyingView];
NSLayoutConstraint *leadingConstraint = [underlyingView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor];
NSLayoutConstraint *trailingConstraint = [underlyingView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor];
NSLayoutConstraint *topConstraint = [underlyingView.topAnchor constraintEqualToAnchor:self.topAnchor];
NSLayoutConstraint *bottomConstraint = [underlyingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor];
NSLayoutConstraint *heightConstraint = [self.heightAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.height];
NSLayoutConstraint *widthConstraint = [self.widthAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.width];
[NSLayoutConstraint activateConstraints:@[leadingConstraint, trailingConstraint, topConstraint, bottomConstraint, heightConstraint, widthConstraint]];
}
return self;
}
@end
当然最简单的方法就是让你的图片变大一些,比如增大留白
与屏幕间距的变化
在 iOS 11 之前,我们有两种方式来修改 item 与屏幕的间距
- 设置一个宽带为负值,类型为 Fixed Space 的 item
- 重写 Custom View 的 alignmentRectInsets 方法
- 如果 Custom View 为 UIButton 类型,可以重写其 contentEdgeInsets/imageEdgeInsets/titleEdgeInsets 并修改其 hitTest 区域
iOS 10 中的解决方案一:使用宽度为负值,类型为 Fixed Space 的 item
当我们使用 Custom View 类型的 item 时,item 与屏幕的边距默认为 16 或者 20 pt(PS:当屏幕为5.5寸屏时,边距为 20 pt),下图以 16 pt 为例:
如果我们希望 item 与屏幕边距为 8pt 时,我们的解决方案通常是这样的:
UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
space.width = -8;
self.navigationItem.rightBarButtonItems = @[space, item1, item2];
这个方案在 iOS 10 及以下的系统是有效的,效果如下图所示:
但是同样的代码在 iOS 11 上就是失效的……
仔细研究 iOS 11 中的视图布局可以发现,fixed space 类型的 item 在 Stack View 的 leading 和 trailling 时的行为与其在 item 之间的行为保持一致,它的最小宽度都为 8pt,这也就说明了设置负数为什么会不生效了。
iOS 10 中的解决方案二:重写 Custom View 的 alignmentRectInsets 方法
我们可以重写某个视图控件的 alignmentRectInsets 属性,例如下面的代码:
- (UIEdgeInsets)alignmentRectInsets {
return self.position == right ? UIEdgeInsetsMake(0, -8, 0, 8) : UIEdgeInsetsMake(0, 8, 0, -8);
}
当我们设置 UIEdgeInsetsMake(0, -8, 0, 8) ,并将这段代码用到前面的例子时,你会看到这样的结果:
乍一看,感觉这个方法似乎解决了所有的问题,但仔细观察,你就会发现这个方案也存在不完美的地方,例如超出 stack view 的部分将无法响应点击事件
iOS 10 中的解决方案三:利用 XXXEdgeInsets 属性来修改
方案三和方案二比较相似,通过修改 XXXEdgeInsets 属性确实能让控件在视觉上达到预期,但由于 stack View 的存在,即使修改了 Custom View 的 hitTest 区域,也会存在无法响应点击事件的问题,所以这个方案也不是一个完美的解决方案:
解决方案
虽然刚才提到的三种解决方案在 iOS 11 中都无法完美解决问题,但我们还是发现了一个有意思的现象
下图是使用宽度为负值,类型为 Fixed Space 的 item 时的视图布局,虽然 fixed space 的存在 让它在视觉上看起来离屏幕边距还是有点远,但 item 控件自身与屏幕的边距确实变小了。
这样说可能让人有点摸不着头脑,我们不妨来看看这中间的区别,下图是使用了非 customView 创建的 UIBarButtonItem,与屏幕的间距为 8 pt(当屏幕为 5.5 寸时,为 12 pt)
下图是使用了 customView 创建的 UIBarButtonItem,与屏幕的间距为 16 pt(当屏幕为 5.5 寸时,为 20 pt)
正是基于以上的观察,我们发现只要在 stack view 中添加一个 非 customView 创建的 UIBarButtonItem, 系统就会给我们减少 item 与屏幕间的距离,如果此时我们再用 alignmentRectInsets 提供一个视觉上的偏移,就可以完美解决当前的问题。
UIButton *customButton = [UIButton buttonWithType:UIButtonTypeCustom];
customButton.overrideAlignmentRectInsets = UIEdgeInsetsMake(0, x, 0, -x); // you should do this in your own custom class
customButton.translatesAutoresizingMaskIntoConstraints = NO;
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:customButton]
UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
positiveSeparator.width = 8;
self.navigationItem.leftBarButtonItems = @{positiveSeparator, item, ...}
不过需要注意的是,这个方法能只能保证 item 距离屏幕的边缘为 8 - 12 pt,如果你想让 item 与屏幕的距离更近一些的话,就可能会出现其他的问题。
常用分类
#import
typedef void (^TLBarButtonActionBlock)();
@interface UIBarButtonItem (Action)
- (id)initWithTitle:(NSString *)title actionBlick:(TLBarButtonActionBlock)actionBlock;
- (id)initWithImage:(UIImage *)image actionBlick:(TLBarButtonActionBlock)actionBlock;
@end
#import "UIBarButtonItem+Action.h"
#import
char * const UIBarButtonItemActionBlock = "UIBarButtonItemActionBlock";
@implementation UIBarButtonItem (Action)
- (id)initWithTitle:(NSString *)title actionBlick:(TLBarButtonActionBlock)actionBlock
{
if (self = [self initWithTitle:title style:UIBarButtonItemStylePlain target:nil action:nil]) {
[self setActionBlock:actionBlock];
}
return self;
}
- (id)initWithImage:(UIImage *)image actionBlick:(TLBarButtonActionBlock)actionBlock
{
if (self = [self initWithImage:image style:UIBarButtonItemStylePlain target:nil action:nil]) {
[self setActionBlock:actionBlock];
}
return self;
}
- (void)performActionBlock {
dispatch_block_t block = self.actionBlock;
if (block) {
block();
}
}
- (TLBarButtonActionBlock)actionBlock {
return objc_getAssociatedObject(self, UIBarButtonItemActionBlock);
}
- (void)setActionBlock:(TLBarButtonActionBlock)actionBlock {
if (actionBlock != self.actionBlock) {
[self willChangeValueForKey:@"actionBlock"];
objc_setAssociatedObject(self, UIBarButtonItemActionBlock, actionBlock, OBJC_ASSOCIATION_COPY);
[self setTarget:self];
[self setAction:@selector(performActionBlock)];
[self didChangeValueForKey:@"actionBlock"];
}
}
@end
参考文章:
UIBarButtonItem 在 iOS 11 上的改变及应对方案