相信很多同學都知道在iOS7之后調整導航欄兩側按鈕距離左右間距,其實就是在左右barButtonItem的數組中添加一個寬度為負的占位item。
- (void)addLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem { UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; space.width = -8; [self setLeftBarButtonItems:@[space, leftBarButtonItem]]; }
但是在iOS11之后,發現該方法失效了。新的思路和iOS7的完全不一樣,我們給UINavigationBarContentView加一條約束。怎么加呢?
1 自定義一個customView,使用initWithCustomView創建UIBarButtonItem。
2 在customView的layoutSubviews方法中找到UINavigationBarContentView,添加customView和UINavigationBarContentView之間的約束。
customView定義如下:
#import "UIView.h" typedef NS_ENUM(NSInteger, LFBarButtonItemViewType) { LFBarButtonItemViewTypeLeft, LFBarButtonItemViewTypeRight, }; @interface LFBarButtonItemView : UIView @property (nonatomic, assign) LFBarButtonItemViewType type; @end
@implementation LFBarButtonItemView - (void)layoutSubviews { [super layoutSubviews]; if (iOSVersion < 11.0) { return; } //Here is a workaround on iOS 11 UINavigationBarItem init with custom view, position issue UIView *view = self; while (![view isKindOfClass:[UINavigationBar class]] && [view superview] != nil) { view = [view superview]; if ([view isKindOfClass:[UIStackView class]] && [view superview] != nil) { if (self.type == LFBarButtonItemViewTypeLeft) { CGFloat margin = 0.0f; // 5.5寸plus間距大一點 if ([[TWDeviceManager sharedManager] iPhone55]) { margin = 4.0f; } [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeLeading multiplier:1.0 constant:margin]]; break; } else if (self.type == LFBarButtonItemViewTypeRight) { CGFloat margin = 0.0f; // 5.5寸plus間距大一點 if ([[TWDeviceManager sharedManager] iPhone55]) { margin = -4.0f; } [view.superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:margin]]; break; } } } }
在創建self.navigationItem.rightBarButtonItem的地方:
- (void)initNavigationBar { // rightItem UIImage *image = [UIImage imageNamed:@"share-icon-dark"]; LFBarButtonItemView *rightItemCustomView = [[LFBarButtonItemView alloc] initWithFrame:CGRectMake(0, 0, image.size.width, image.size.height)]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didClickOnShareButton)]; [rightItemCustomView addGestureRecognizer:tap]; rightItemCustomView.type = LFBarButtonItemViewTypeRight; UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; [imageView sizeToFit]; [rightItemCustomView addSubview:imageView]; UIBarButtonItem *rightItem = [[UIBarButtonItem alloc] initWithCustomView:rightItemCustomView]; self.navigationItem.rightBarButtonItem = rightItem; }
但是這個方法會有約束沖突問題,所以我們把產生沖突的約束刪除。可以用xcode查看視圖層次,以方便理解。
- (void)layoutSubviews { [super layoutSubviews]; if (iOSVersion < 11.0) { return; } //Here is a workaround on iOS 11 UINavigationBarItem init with custom view, position issue UIView *view = self; while (![view isKindOfClass:[UINavigationBar class]] && [view superview] != nil) { view = [view superview]; if ([view isKindOfClass:[UIStackView class]] && [view superview] != nil) { if (self.type == LFBarButtonItemViewTypeLeft) { CGFloat margin = kAppAdaptHeight(15); //刪除原來的leading約束 for (NSLayoutConstraint *constraint in view.superview.constraints) { if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && constraint.firstAttribute == NSLayoutAttributeTrailing) { [view.superview removeConstraint:constraint]; } } //添加新約束 NSLayoutConstraint *leadingConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeLeading multiplier:1.0 constant:margin]; leadingConstraint.priority = UILayoutPriorityRequired; [view.superview addConstraint:leadingConstraint]; break; } else if (self.type == LFBarButtonItemViewTypeRight) { CGFloat margin = -kAppAdaptHeight(15); //刪除原來的leading約束 for (NSLayoutConstraint *constraint in view.superview.constraints) { if ([constraint.firstItem isKindOfClass:[UILayoutGuide class]] && constraint.firstAttribute == NSLayoutAttributeTrailing) { [view.superview removeConstraint:constraint]; } } NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:margin]; trailingConstraint.priority = UILayoutPriorityRequired; [view.superview addConstraint:trailingConstraint]; break; } } } }
現在看起來問題解決了,但是某一個界面在push一個新界面之后再返回回來之后位置就還原了
解決方案其實很簡單,只要將設置leftItem的方法寫在viewWillAppear中即可,這樣即可保證約束不會被系統重置。但是,這樣的方案,是不是覺得不完美!
現在有一個終極解決方案:
UINavigationBarContentView平鋪在導航欄中作為iOS11的各個按鈕的父視圖,該視圖的所有的子視圖都會有一個layoutMargins被占用,也就是系統調整的占位,我們只要把這個置空就行了.那樣的話該視圖下的所有的子視圖的空間就會變成我們想要的那樣,當然為了保險起見,該視圖的父視圖也就是bar的layoutMargins也置空,這樣 整個bar就會跟一個普通視圖一樣了 左右的占位約束就不存在了
給UINavigationBar寫一個分類:
#import "UINavigationBar+iOS11Spacing.h" #import <objc/runtime.h> #define kSpacerWidth kAppAdaptWidth(15) @implementation UINavigationBar (iOS11Spacing) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethodWithOriginSel:@selector(layoutSubviews) swizzledSel:@selector(sx_layoutSubviews)]; }); } -(void)sx_layoutSubviews{ [self sx_layoutSubviews]; if (iOS11_OR_LATER && !kSpacerWidth) {//需要調節 self.layoutMargins = UIEdgeInsetsZero; CGFloat space = kSpacerWidth; for (UIView *subview in self.subviews) { if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) { subview.layoutMargins = UIEdgeInsetsMake(0, space, 0, space);//可修正iOS11之后的偏移 break; } } } } + (void)swizzleInstanceMethodWithOriginSel:(SEL)oriSel swizzledSel:(SEL)swiSel { Method originAddObserverMethod = class_getInstanceMethod(self, oriSel); Method swizzledAddObserverMethod = class_getInstanceMethod(self, swiSel); [self swizzleMethodWithOriginSel:oriSel oriMethod:originAddObserverMethod swizzledSel:swiSel swizzledMethod:swizzledAddObserverMethod class:self]; } + (void)swizzleMethodWithOriginSel:(SEL)oriSel oriMethod:(Method)oriMethod swizzledSel:(SEL)swizzledSel swizzledMethod:(Method)swizzledMethod class:(Class)cls { BOOL didAddMethod = class_addMethod(cls, oriSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(cls, swizzledSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); } else { method_exchangeImplementations(oriMethod, swizzledMethod); } }
這樣就有一個好處,在原來代碼的基礎上,判斷iOS11,什么都不做,iOS7-iOS11之間版本使用老方法修改間距。
舉一個設置LeftBarButtonItem的例子:
#import "UINavigationItem+iOS7Spacing.h"
#import <objc/runtime.h>
#define xSpacerWidth -8
@implementation UINavigationItem (iOS7Spacing)
- (UIBarButtonItem *)spacer
{
UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
space.width = xSpacerWidth;
return space;
}
- (void)mk_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem
{
if (iOS11_OR_LATER) {
[self mk_setLeftBarButtonItem:leftBarButtonItem];
} else if (iOS7_OR_LATER) {
if (leftBarButtonItem && (leftBarButtonItem.customView !=nil || leftBarButtonItem.image !=nil)) {
[self mk_setLeftBarButtonItem:nil];
[self mk_setLeftBarButtonItems:@[[self spacer], leftBarButtonItem]];
} else {
if (iOS7_OR_LATER) {
[self mk_setLeftBarButtonItems:nil];
}
[self mk_setLeftBarButtonItem:leftBarButtonItem];
}
} else {
[self mk_setLeftBarButtonItem:leftBarButtonItem];
}
}
- (void)mk_setLeftBarButtonItems:(NSArray *)leftBarButtonItems
{
if (iOS7_OR_LATER && leftBarButtonItems && leftBarButtonItems.count > 0 ) {
NSMutableArray *items = [[NSMutableArray alloc] initWithCapacity:leftBarButtonItems.count + 1];
[items addObject:[self spacer]];
[items addObjectsFromArray:leftBarButtonItems];
[self mk_setLeftBarButtonItems:items];
} else {
[self mk_setLeftBarButtonItems:leftBarButtonItems];
}
}
- (void)mk_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem
{
if (iOS11_OR_LATER) {
[self mk_setRightBarButtonItem:rightBarButtonItem];
} else if (iOS7_OR_LATER) {
if (rightBarButtonItem && (rightBarButtonItem.customView !=nil || rightBarButtonItem.image != nil)) {
[self mk_setRightBarButtonItem:nil];
[self mk_setRightBarButtonItems:@[[self spacer], rightBarButtonItem]];
} else {
if (iOS7_OR_LATER) {
[self mk_setRightBarButtonItems:nil];
}
[self mk_setRightBarButtonItem:rightBarButtonItem];
}
} else {
[self mk_setRightBarButtonItem:rightBarButtonItem];
}
}
- (void)mk_setRightBarButtonItems:(NSArray *)rightBarButtonItems
{
if (iOS7_OR_LATER && rightBarButtonItems && rightBarButtonItems.count > 0) {
NSMutableArray *items = [[NSMutableArray alloc] initWithCapacity:rightBarButtonItems.count + 1];
[items addObject:[self spacer]];
[items addObjectsFromArray:rightBarButtonItems];
[self mk_setRightBarButtonItems:items];
} else {
[self mk_setRightBarButtonItems:rightBarButtonItems];
}
}
+ (void)mk_swizzle:(SEL)aSelector
{
SEL bSelector = NSSelectorFromString([NSString stringWithFormat:@"mk_%@", NSStringFromSelector(aSelector)]);
Method m1 = class_getInstanceMethod(self, aSelector);
Method m2 = class_getInstanceMethod(self, bSelector);
method_exchangeImplementations(m1, m2);
}
+ (void)load
{
[self mk_swizzle:@selector(setLeftBarButtonItem:)];
[self mk_swizzle:@selector(setLeftBarButtonItems:)];
[self mk_swizzle:@selector(setRightBarButtonItem:)];
[self mk_swizzle:@selector(setRightBarButtonItems:)];
}
參考文章: iOS11 導航欄按鈕位置問題的解決