You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

324 lines
17 KiB

/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
* Copyright (C) 2016-2020 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
//
// UITabBar+QMUI.m
// qmui
//
// Created by QMUI Team on 2017/2/14.
//
#import "UITabBar+QMUI.h"
#import "QMUICore.h"
#import "UITabBarItem+QMUI.h"
#import "UIBarItem+QMUI.h"
#import "UIImage+QMUI.h"
#import "UIView+QMUI.h"
NSInteger const kLastTouchedTabBarItemIndexNone = -1;
@interface UITabBar ()
@property(nonatomic, assign) BOOL canItemRespondDoubleTouch;
@property(nonatomic, assign) NSInteger lastTouchedTabBarItemViewIndex;
@property(nonatomic, assign) NSInteger tabBarItemViewTouchCount;
@end
@implementation UITabBar (QMUI)
QMUISynthesizeBOOLProperty(canItemRespondDoubleTouch, setCanItemRespondDoubleTouch)
QMUISynthesizeNSIntegerProperty(lastTouchedTabBarItemViewIndex, setLastTouchedTabBarItemViewIndex)
QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouchCount)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ExtendImplementationOfNonVoidMethodWithSingleArgument([UITabBar class], @selector(initWithFrame:), CGRect, UITabBar *, ^UITabBar *(UITabBar *selfObject, CGRect frame, UITabBar *originReturnValue) {
if (QMUICMIActivated) {
if (@available(iOS 13.0, *)) {
// iOS 13 不使用 tintColor 了,改为用 UITabBarAppearance,具体请看 QMUIConfiguration.m
} else {
// UIView.tintColor 并不支持 UIAppearance 协议,所以不能通过 appearance 来设置,只能在实例里设置
selfObject.tintColor = TabBarItemImageColorSelected;
}
}
return originReturnValue;
});
OverrideImplementation([UITabBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^void(UITabBar *selfObject, NSArray<UITabBarItem *> *items, BOOL animated) {
// 应用配置表样式
[items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
if (QMUICMIActivated) {
if (@available(iOS 13.0, *)) {
// iOS 13 通过 appearance 的方式修改,具体请查看 QMUIConfiguration.m
} else {
[item qmui_updateTintColorForiOS12AndEarlier:TabBarItemImageColor];
}
}
}];
// call super
void (*originSelectorIMP)(id, SEL, NSArray<UITabBarItem *> *, BOOL);
originSelectorIMP = (void (*)(id, SEL, NSArray<UITabBarItem *> *, BOOL))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, items, animated);
[items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
// 双击 tabBarItem 的功能需要在设置完 item 后才能获取到 qmui_view 来实现
UIControl *itemView = (UIControl *)item.qmui_view;
[itemView addTarget:selfObject action:@selector(handleTabBarItemViewEvent:) forControlEvents:UIControlEventTouchUpInside];
}];
};
});
OverrideImplementation([UITabBar class], @selector(setSelectedItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UITabBar *selfObject, UITabBarItem *selectedItem) {
NSInteger olderSelectedIndex = selfObject.selectedItem ? [selfObject.items indexOfObject:selfObject.selectedItem] : -1;
// call super
void (*originSelectorIMP)(id, SEL, UITabBarItem *);
originSelectorIMP = (void (*)(id, SEL, UITabBarItem *))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, selectedItem);
NSInteger newerSelectedIndex = [selfObject.items indexOfObject:selectedItem];
// 只有双击当前正在显示的界面的 tabBarItem,才能正常触发双击事件
selfObject.canItemRespondDoubleTouch = olderSelectedIndex == newerSelectedIndex;
};
});
OverrideImplementation([UITabBar class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UITabBar *selfObject, CGRect frame) {
if (IOS_VERSION < 11.2 && IS_58INCH_SCREEN && ShouldFixTabBarTransitionBugInIPhoneX) {
if (CGRectGetHeight(frame) == TabBarHeight && CGRectGetMaxY(frame) < CGRectGetHeight(selfObject.superview.bounds)) {
// iOS 11 在界面 push 的过程中 tabBar 会瞬间往上跳,所以做这个修复。这个 bugiOS 11.2 里已被系统修复。
// https://github.com/Tencent/QMUI_iOS/issues/217
frame = CGRectSetY(frame, CGRectGetHeight(selfObject.superview.bounds) - CGRectGetHeight(frame));
}
}
// 修复这个 bughttps://github.com/Tencent/QMUI_iOS/issues/309
if (@available(iOS 11, *)) {
if (IS_NOTCHED_SCREEN && ((CGRectGetHeight(frame) == 49 || CGRectGetHeight(frame) == 32))) {// 只关注全面屏设备下的这两种非正常的 tabBar 高度即可
CGFloat bottomSafeAreaInsets = selfObject.safeAreaInsets.bottom > 0 ? selfObject.safeAreaInsets.bottom : selfObject.superview.safeAreaInsets.bottom;// 注意,如果只是拿 selfObject.safeAreaInsets 判断,会肉眼看到高度的跳变,因此引入 superview 的值(虽然理论上 tabBar 不一定都会布局到 UITabBarController.view 的底部)
if (bottomSafeAreaInsets == CGRectGetHeight(selfObject.frame)) {
return;// 由于这个系统 bug https://github.com/Tencent/QMUI_iOS/issues/446,这里先暂时屏蔽本次 frame 变化
}
frame.size.height += bottomSafeAreaInsets;
frame.origin.y -= bottomSafeAreaInsets;
}
}
// call super
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, frame);
};
});
// 以下代码修复两个仅存在于 12.1.0 版本的系统 bug,实测 12.1.1 苹果已经修复
if (@available(iOS 12.1, *)) {
OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UIView *selfObject, CGRect firstArgv) {
// Fixed: UITabBar layout is broken on iOS 12.1
// https://github.com/Tencent/QMUI_iOS/issues/410
if (IOS_VERSION_NUMBER < 120101 || (QMUICMIActivated && ShouldFixTabBarButtonBugForAll)) {
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
}
if (IOS_VERSION_NUMBER < 120101) {
// Fixed: iOS 12.1 UITabBarItem positioning issue during swipe back gesture (when UINavigationBar is hidden)
// https://github.com/Tencent/QMUI_iOS/issues/422
if (IS_NOTCHED_SCREEN) {
if ((CGRectGetHeight(selfObject.frame) == 48 && CGRectGetHeight(firstArgv) == 33) || (CGRectGetHeight(selfObject.frame) == 31 && CGRectGetHeight(firstArgv) == 20)) {
return;
}
}
}
// call super
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, firstArgv);
};
});
}
// iOS 13 下如果以 UITabBarAppearance 的方式将 UITabBarItemfont 大小设置为超过默认的 10,则会出现布局错误,文字被截断,所以这里做了个兼容
// https://github.com/Tencent/QMUI_iOS/issues/740
if (@available(iOS 13.0, *)) {
OverrideImplementation(NSClassFromString(@"UITabBarButtonLabel"), @selector(setAttributedText:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^(UILabel *selfObject, NSAttributedString *firstArgv) {
// call super
void (*originSelectorIMP)(id, SEL, NSAttributedString *);
originSelectorIMP = (void (*)(id, SEL, NSAttributedString *))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, firstArgv);
CGFloat fontSize = selfObject.font.pointSize;
if (fontSize > 10) {
[selfObject sizeToFit];
}
};
});
}
// 以下是将 iOS 12 修改 UITabBar 样式的接口转换成用 iOS 13 的新接口去设置(因为新旧方法是互斥的,所以统一在新系统都用新方法)
// 但这样有个风险,因为 QMUIConfiguration 配置表里都是用 appearance 的方式去设置 standardAppearance,所以如果在 UITabBar 实例被添加到 window 之前修改过旧版任意一个样式接口,就会导致一个新的 UITabBarAppearance 对象被设置给 standardAppearance 属性,这样系统就会认为你这个 UITabBar 实例自定义了 standardAppearance,那么当它被 moveToWindow 时就不会自动应用 appearance 的值了,因此需要保证在添加到 window 前不要自行修改属性
#ifdef IOS13_SDK_ALLOWED
if (@available(iOS 13.0, *)) {
void (^syncAppearance)(UITabBar *, void(^barActionBlock)(UITabBarAppearance *appearance), void (^itemActionBlock)(UITabBarItemAppearance *itemAppearance)) = ^void(UITabBar *tabBar, void(^barActionBlock)(UITabBarAppearance *appearance), void (^itemActionBlock)(UITabBarItemAppearance *itemAppearance)) {
if (!barActionBlock && !itemActionBlock) return;
UITabBarAppearance *appearance = tabBar.standardAppearance;
if (barActionBlock) {
barActionBlock(appearance);
}
if (itemActionBlock) {
[appearance qmui_applyItemAppearanceWithBlock:itemActionBlock];
}
tabBar.standardAppearance = appearance;
};
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) {
syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) {
itemAppearance.selected.iconColor = tintColor;
NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = itemAppearance.selected.titleTextAttributes.mutableCopy;
textAttributes[NSForegroundColorAttributeName] = tintColor;
itemAppearance.selected.titleTextAttributes = textAttributes.copy;
});
});
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *barTintColor) {
syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
appearance.backgroundColor = barTintColor;
}, nil);
});
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setUnselectedItemTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) {
syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) {
itemAppearance.normal.iconColor = tintColor;
NSMutableDictionary *textAttributes = itemAppearance.selected.titleTextAttributes.mutableCopy;
textAttributes[NSForegroundColorAttributeName] = tintColor;
itemAppearance.normal.titleTextAttributes = textAttributes.copy;
});
});
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBackgroundImage:), UIImage *, ^(UITabBar *selfObject, UIImage *image) {
syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
appearance.backgroundImage = image;
}, nil);
});
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setShadowImage:), UIImage *, ^(UITabBar *selfObject, UIImage *shadowImage) {
syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
appearance.shadowImage = shadowImage;
}, nil);
});
ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarStyle:), UIBarStyle, ^(UITabBar *selfObject, UIBarStyle barStyle) {
syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemMaterialLight : UIBlurEffectStyleSystemMaterialDark];
}, nil);
});
}
#endif
});
}
- (UIView *)qmui_backgroundView {
return [self qmui_valueForKey:@"_backgroundView"];
}
- (UIImageView *)qmui_shadowImageView {
if (@available(iOS 13, *)) {
return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"];
} else if (@available(iOS 10, *)) {
// iOS 10 及以后,在 UITabBar 初始化之后就能获取到 backgroundViewshadowView
return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"];
}
// iOS 9 及以前,shadowView 要在 UITabBar 第一次 layoutSubviews 之后才会被创建,直至 UITabBarController viewWillAppear: 时仍未能获取到 shadowView,所以为了省去调用时机的考虑,这里获取不到的时候会主动触发一次 tabBar 的布局
UIImageView *shadowView = [self qmui_valueForKey:@"_shadowView"];
if (!shadowView) {
[self setNeedsLayout];
[self layoutIfNeeded];
shadowView = [self qmui_valueForKey:@"_shadowView"];
}
return shadowView;
}
- (void)handleTabBarItemViewEvent:(UIControl *)itemView {
if (!self.canItemRespondDoubleTouch) {
return;
}
if (!self.selectedItem.qmui_doubleTapBlock) {
return;
}
// 如果一定时间后仍未触发双击,则废弃当前的点击状态
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self revertTabBarItemTouch];
});
NSInteger selectedIndex = [self.items indexOfObject:self.selectedItem];
if (self.lastTouchedTabBarItemViewIndex == kLastTouchedTabBarItemIndexNone) {
// 记录第一次点击的 index
self.lastTouchedTabBarItemViewIndex = selectedIndex;
} else if (self.lastTouchedTabBarItemViewIndex != selectedIndex) {
// 后续的点击如果与第一次点击的 index 不一致,则认为是重新开始一次新的点击
[self revertTabBarItemTouch];
self.lastTouchedTabBarItemViewIndex = selectedIndex;
return;
}
self.tabBarItemViewTouchCount ++;
if (self.tabBarItemViewTouchCount == 2) {
// 第二次点击了相同的 tabBarItem,触发双击事件
UITabBarItem *item = self.items[selectedIndex];
if (item.qmui_doubleTapBlock) {
item.qmui_doubleTapBlock(item, selectedIndex);
}
[self revertTabBarItemTouch];
}
}
- (void)revertTabBarItemTouch {
self.lastTouchedTabBarItemViewIndex = kLastTouchedTabBarItemIndexNone;
self.tabBarItemViewTouchCount = 0;
}
@end
#ifdef IOS13_SDK_ALLOWED
@implementation UITabBarAppearance (QMUI)
- (void)qmui_applyItemAppearanceWithBlock:(void (^)(UITabBarItemAppearance * _Nonnull))block {
block(self.stackedLayoutAppearance);
block(self.inlineLayoutAppearance);
block(self.compactInlineLayoutAppearance);
}
@end
#endif