iOS 12.1 后退导航上的 UITabBar 项目
UITabBar items jumping on back navigation on iOS 12.1
我有一个 iOS 应用程序,主屏幕上有 UITabBarController
,导航到详细信息屏幕隐藏 UITabBarController
,设置为 hidesBottomBarWhenPushed = true
。
当返回到主屏幕时,UITabBarController
会出现一个奇怪的 "jump",如这个 GIF 所示:
这 仅在 iOS 12.1 上发生,不会在 12.0 或 11.x.
上发生
似乎是 iOS 12.1 错误,因为我注意到 FB Messenger 等其他应用程序也有这种行为,但我想知道,是否有某种解决方法?
估计是苹果的bug
但是您可以尝试将此作为热修复:只需使用以下代码为您的 tabBar 创建一个 class:
import UIKit
class FixedTabBar: UITabBar {
var itemFrames = [CGRect]()
var tabBarItems = [UIView]()
override func layoutSubviews() {
super.layoutSubviews()
if itemFrames.isEmpty, let UITabBarButtonClass = NSClassFromString("UITabBarButton") as? NSObject.Type {
tabBarItems = subviews.filter({[=10=].isKind(of: UITabBarButtonClass)})
tabBarItems.forEach({itemFrames.append([=10=].frame)})
}
if !itemFrames.isEmpty, !tabBarItems.isEmpty, itemFrames.count == items?.count {
tabBarItems.enumerated().forEach({[=10=].element.frame = itemFrames[[=10=].offset]})
}
}
}
import UIKit
extension UITabBar{
open override func layoutSubviews() {
super.layoutSubviews()
if let UITabBarButtonClass = NSClassFromString("UITabBarButton") as? NSObject.Type{
let subItems = self.subviews.filter({return [=10=].isKind(of: UITabBarButtonClass)})
if subItems.count > 0{
let tmpWidth = UIScreen.main.bounds.width / CGFloat(subItems.count)
for (index,item) in subItems.enumerated(){
item.frame = CGRect(x: CGFloat(index) * tmpWidth, y: 0, width: tmpWidth, height: item.bounds.height)
}
}
}
}
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let view:UITabBar = super.hitTest(point, with: event) as? UITabBar{
for item in view.subviews{
if point.x >= item.frame.origin.x && point.x <= item.frame.origin.x + item.frame.size.width{
return item
}
}
}
return super.hitTest(point, with: event)
}
}
有两种方法可以解决这个问题,
首先,在您的 UITabBarController 中,将 isTranslucent = false 设置为:
[[UITabBar appearance] setTranslucent:NO];
其次,如果第一个解决方案不能解决您的问题,请尝试以下方法:
这是Objective-C代码
// .h
@interface CYLTabBar : UITabBar
@end
// .m
#import "CYLTabBar.h"
CG_INLINE BOOL
OverrideImplementation(Class targetClass, SEL targetSelector, id (^implementationBlock)(Class originClass, SEL originCMD, IMP originIMP)) {
Method originMethod = class_getInstanceMethod(targetClass, targetSelector);
if (!originMethod) {
return NO;
}
IMP originIMP = method_getImplementation(originMethod);
method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originIMP)));
return YES;
}
@implementation CYLTabBar
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 12.1, *)) {
OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP originIMP) {
return ^(UIView *selfObject, CGRect firstArgv) {
if ([selfObject isKindOfClass:originClass]) {
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
}
// call super
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originIMP;
originSelectorIMP(selfObject, originCMD, firstArgv);
};
});
}
});
}
@end
更多信息:https://github.com/ChenYilong/CYLTabBarController/commit/2c741c8bffd47763ad2fca198202946a2a63c4fc
这是一个可以处理旋转和添加或删除标签栏项目的解决方案:
class FixedTabBar: UITabBar {
var buttonFrames: [CGRect] = []
var size: CGSize = .zero
override func layoutSubviews() {
super.layoutSubviews()
if UIDevice.current.systemVersion >= "12.1" {
let buttons = subviews.filter {
String(describing: type(of: [=10=])).hasSuffix("Button")
}
if buttonFrames.count == buttons.count, size == bounds.size {
zip(buttons, buttonFrames).forEach { [=10=].0.frame = [=10=].1 }
} else {
buttonFrames = buttons.map { [=10=].frame }
size = bounds.size
}
}
}
}
感谢 的想法,
我只是将 c 内联函数更改为 OC 静态方法,因为我不会过多地使用此 overrideImplementation
。此外,此代码片段现已针对 iPhoneX 进行了调整。
static CGFloat const kIPhoneXTabbarButtonErrorHeight = 33;
static CGFloat const kIPhoneXTabbarButtonHeight = 48;
@implementation FixedTabBar
typedef void(^NewTabBarButtonFrameSetter)(UIView *, CGRect);
typedef NewTabBarButtonFrameSetter (^ImpBlock)(Class originClass, SEL originCMD, IMP originIMP);
+ (BOOL)overrideImplementationWithTargetClass:(Class)targetClass targetSelector:(SEL)targetSelector implementBlock:(ImpBlock)implementationBlock {
Method originMethod = class_getInstanceMethod(targetClass, targetSelector);
if (!originMethod) {
return NO;
}
IMP originIMP = method_getImplementation(originMethod);
method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originIMP)));
return YES;
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 12.1, *)) {
[self overrideImplementationWithTargetClass:NSClassFromString(@"UITabBarButton")
targetSelector:@selector(setFrame:)
implementBlock:^NewTabBarButtonFrameSetter(__unsafe_unretained Class originClass, SEL originCMD, IMP originIMP) {
return ^(UIView *selfObject, CGRect firstArgv) {
if ([selfObject isKindOfClass:originClass]) {
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
if (firstArgv.size.height == kIPhoneXTabbarButtonErrorHeight) {
firstArgv.size.height = kIPhoneXTabbarButtonHeight;
}
}
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originIMP;
originSelectorIMP(selfObject, originCMD, firstArgv);
};
}];
}
});
}
@end
在您的 UITabBarController
中,设置 isTranslucent = false
这里是swift代码
extension UIApplication {
open override var next: UIResponder? {
// Called before applicationDidFinishLaunching
SwizzlingHelper.enableInjection()
return super.next
}
}
class SwizzlingHelper {
static func enableInjection() {
DispatchQueue.once(token: "com.SwizzlingInjection") {
//what to need inject
UITabbarButtonInjection.inject()
}
}
更多信息 https://github.com/tonySwiftDev/UITabbar-fixIOS12.1Bug
我遇到了完全相同的问题,该应用程序的架构是每个选项卡都有一个导航控制器。我发现解决此问题的最简单 non-hacky 方法是将 UITabBarController
放在 UINavigationController
中,然后删除单个 UINavigationController
s.
之前:
-> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
UITabBarController -> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
之后:
-> UIViewController
-> UIViewController
UINavigationController -> UITabBarController -> UIViewController
-> UIViewController
-> UIViewController
通过使用外部 UINavigationController
,将视图控制器推送到导航堆栈时,您无需隐藏 UITabBar
。
警告:
到目前为止我发现的唯一问题是,在每个 UIViewController
上设置标题或 right/left 条形按钮项目不会产生相同的效果。为了克服这个问题,我在可见的 UIViewController
发生变化时通过 UITabBarControllerDelegate
应用了更改。
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard let topItem = self.navigationController?.navigationBar.topItem else { return }
precondition(self.navigationController == viewController.navigationController, "Navigation controllers do not match. The following changes might result in unexpected behaviour.")
topItem.title = viewController.title
topItem.titleView = viewController.navigationItem.titleView
topItem.leftBarButtonItem = viewController.navigationItem.leftBarButtonItem
topItem.rightBarButtonItem = viewController.navigationItem.rightBarButtonItem
}
请注意,我添加了一个 preconditionFailure
以捕捉导航架构已被修改的任何情况
Apple 现已在 iOS 12.1.1
中修复了该问题
如果您仍想保持标签栏半透明,您需要从 UITabBar
继承并覆盖 属性 safeAreaInsets
class MyTabBar: UITabBar {
private var safeInsets = UIEdgeInsets.zero
@available(iOS 11.0, *)
override var safeAreaInsets: UIEdgeInsets {
set {
if newValue != UIEdgeInsets.zero {
safeInsets = newValue
}
}
get {
return safeInsets
}
}
}
想法是不允许系统设置zero
insets,这样tab bar就不会跳转了。
你可以覆盖 - (UIEdgeInsets)safeAreaInsets
方法来进行少数 iOS 12 次颠覆:
- (UIEdgeInsets)safeAreaInsets {
UIEdgeInsets insets = [super safeAreaInsets];
CGFloat h = CGRectGetHeight(self.frame);
if (insets.bottom >= h) {
insets.bottom = [self.window safeAreaInsets].bottom;
}
return insets;
}
就我而言 (iOS 12.1.4),我发现这种奇怪的故障行为是由 .modalPresentationStyle = .fullScreen
呈现的模态触发的
将他们的 presentationStyle 更新为 .overFullScreen
后,故障消失了。
我有一个 iOS 应用程序,主屏幕上有 UITabBarController
,导航到详细信息屏幕隐藏 UITabBarController
,设置为 hidesBottomBarWhenPushed = true
。
当返回到主屏幕时,UITabBarController
会出现一个奇怪的 "jump",如这个 GIF 所示:
这 仅在 iOS 12.1 上发生,不会在 12.0 或 11.x.
上发生似乎是 iOS 12.1 错误,因为我注意到 FB Messenger 等其他应用程序也有这种行为,但我想知道,是否有某种解决方法?
估计是苹果的bug 但是您可以尝试将此作为热修复:只需使用以下代码为您的 tabBar 创建一个 class:
import UIKit
class FixedTabBar: UITabBar {
var itemFrames = [CGRect]()
var tabBarItems = [UIView]()
override func layoutSubviews() {
super.layoutSubviews()
if itemFrames.isEmpty, let UITabBarButtonClass = NSClassFromString("UITabBarButton") as? NSObject.Type {
tabBarItems = subviews.filter({[=10=].isKind(of: UITabBarButtonClass)})
tabBarItems.forEach({itemFrames.append([=10=].frame)})
}
if !itemFrames.isEmpty, !tabBarItems.isEmpty, itemFrames.count == items?.count {
tabBarItems.enumerated().forEach({[=10=].element.frame = itemFrames[[=10=].offset]})
}
}
}
import UIKit
extension UITabBar{
open override func layoutSubviews() {
super.layoutSubviews()
if let UITabBarButtonClass = NSClassFromString("UITabBarButton") as? NSObject.Type{
let subItems = self.subviews.filter({return [=10=].isKind(of: UITabBarButtonClass)})
if subItems.count > 0{
let tmpWidth = UIScreen.main.bounds.width / CGFloat(subItems.count)
for (index,item) in subItems.enumerated(){
item.frame = CGRect(x: CGFloat(index) * tmpWidth, y: 0, width: tmpWidth, height: item.bounds.height)
}
}
}
}
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let view:UITabBar = super.hitTest(point, with: event) as? UITabBar{
for item in view.subviews{
if point.x >= item.frame.origin.x && point.x <= item.frame.origin.x + item.frame.size.width{
return item
}
}
}
return super.hitTest(point, with: event)
}
}
有两种方法可以解决这个问题, 首先,在您的 UITabBarController 中,将 isTranslucent = false 设置为:
[[UITabBar appearance] setTranslucent:NO];
其次,如果第一个解决方案不能解决您的问题,请尝试以下方法:
这是Objective-C代码
// .h
@interface CYLTabBar : UITabBar
@end
// .m
#import "CYLTabBar.h"
CG_INLINE BOOL
OverrideImplementation(Class targetClass, SEL targetSelector, id (^implementationBlock)(Class originClass, SEL originCMD, IMP originIMP)) {
Method originMethod = class_getInstanceMethod(targetClass, targetSelector);
if (!originMethod) {
return NO;
}
IMP originIMP = method_getImplementation(originMethod);
method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originIMP)));
return YES;
}
@implementation CYLTabBar
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 12.1, *)) {
OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP originIMP) {
return ^(UIView *selfObject, CGRect firstArgv) {
if ([selfObject isKindOfClass:originClass]) {
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
}
// call super
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originIMP;
originSelectorIMP(selfObject, originCMD, firstArgv);
};
});
}
});
}
@end
更多信息:https://github.com/ChenYilong/CYLTabBarController/commit/2c741c8bffd47763ad2fca198202946a2a63c4fc
这是一个可以处理旋转和添加或删除标签栏项目的解决方案:
class FixedTabBar: UITabBar {
var buttonFrames: [CGRect] = []
var size: CGSize = .zero
override func layoutSubviews() {
super.layoutSubviews()
if UIDevice.current.systemVersion >= "12.1" {
let buttons = subviews.filter {
String(describing: type(of: [=10=])).hasSuffix("Button")
}
if buttonFrames.count == buttons.count, size == bounds.size {
zip(buttons, buttonFrames).forEach { [=10=].0.frame = [=10=].1 }
} else {
buttonFrames = buttons.map { [=10=].frame }
size = bounds.size
}
}
}
}
感谢 overrideImplementation
。此外,此代码片段现已针对 iPhoneX 进行了调整。
static CGFloat const kIPhoneXTabbarButtonErrorHeight = 33;
static CGFloat const kIPhoneXTabbarButtonHeight = 48;
@implementation FixedTabBar
typedef void(^NewTabBarButtonFrameSetter)(UIView *, CGRect);
typedef NewTabBarButtonFrameSetter (^ImpBlock)(Class originClass, SEL originCMD, IMP originIMP);
+ (BOOL)overrideImplementationWithTargetClass:(Class)targetClass targetSelector:(SEL)targetSelector implementBlock:(ImpBlock)implementationBlock {
Method originMethod = class_getInstanceMethod(targetClass, targetSelector);
if (!originMethod) {
return NO;
}
IMP originIMP = method_getImplementation(originMethod);
method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originIMP)));
return YES;
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 12.1, *)) {
[self overrideImplementationWithTargetClass:NSClassFromString(@"UITabBarButton")
targetSelector:@selector(setFrame:)
implementBlock:^NewTabBarButtonFrameSetter(__unsafe_unretained Class originClass, SEL originCMD, IMP originIMP) {
return ^(UIView *selfObject, CGRect firstArgv) {
if ([selfObject isKindOfClass:originClass]) {
if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
return;
}
if (firstArgv.size.height == kIPhoneXTabbarButtonErrorHeight) {
firstArgv.size.height = kIPhoneXTabbarButtonHeight;
}
}
void (*originSelectorIMP)(id, SEL, CGRect);
originSelectorIMP = (void (*)(id, SEL, CGRect))originIMP;
originSelectorIMP(selfObject, originCMD, firstArgv);
};
}];
}
});
}
@end
在您的 UITabBarController
中,设置 isTranslucent = false
这里是swift代码
extension UIApplication {
open override var next: UIResponder? {
// Called before applicationDidFinishLaunching
SwizzlingHelper.enableInjection()
return super.next
}
}
class SwizzlingHelper {
static func enableInjection() {
DispatchQueue.once(token: "com.SwizzlingInjection") {
//what to need inject
UITabbarButtonInjection.inject()
}
} 更多信息 https://github.com/tonySwiftDev/UITabbar-fixIOS12.1Bug
我遇到了完全相同的问题,该应用程序的架构是每个选项卡都有一个导航控制器。我发现解决此问题的最简单 non-hacky 方法是将 UITabBarController
放在 UINavigationController
中,然后删除单个 UINavigationController
s.
之前:
-> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
UITabBarController -> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
-> UINavigationController -> UIViewController
之后:
-> UIViewController
-> UIViewController
UINavigationController -> UITabBarController -> UIViewController
-> UIViewController
-> UIViewController
通过使用外部 UINavigationController
,将视图控制器推送到导航堆栈时,您无需隐藏 UITabBar
。
警告:
到目前为止我发现的唯一问题是,在每个 UIViewController
上设置标题或 right/left 条形按钮项目不会产生相同的效果。为了克服这个问题,我在可见的 UIViewController
发生变化时通过 UITabBarControllerDelegate
应用了更改。
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard let topItem = self.navigationController?.navigationBar.topItem else { return }
precondition(self.navigationController == viewController.navigationController, "Navigation controllers do not match. The following changes might result in unexpected behaviour.")
topItem.title = viewController.title
topItem.titleView = viewController.navigationItem.titleView
topItem.leftBarButtonItem = viewController.navigationItem.leftBarButtonItem
topItem.rightBarButtonItem = viewController.navigationItem.rightBarButtonItem
}
请注意,我添加了一个 preconditionFailure
以捕捉导航架构已被修改的任何情况
Apple 现已在 iOS 12.1.1
中修复了该问题如果您仍想保持标签栏半透明,您需要从 UITabBar
继承并覆盖 属性 safeAreaInsets
class MyTabBar: UITabBar {
private var safeInsets = UIEdgeInsets.zero
@available(iOS 11.0, *)
override var safeAreaInsets: UIEdgeInsets {
set {
if newValue != UIEdgeInsets.zero {
safeInsets = newValue
}
}
get {
return safeInsets
}
}
}
想法是不允许系统设置zero
insets,这样tab bar就不会跳转了。
你可以覆盖 - (UIEdgeInsets)safeAreaInsets
方法来进行少数 iOS 12 次颠覆:
- (UIEdgeInsets)safeAreaInsets {
UIEdgeInsets insets = [super safeAreaInsets];
CGFloat h = CGRectGetHeight(self.frame);
if (insets.bottom >= h) {
insets.bottom = [self.window safeAreaInsets].bottom;
}
return insets;
}
就我而言 (iOS 12.1.4),我发现这种奇怪的故障行为是由 .modalPresentationStyle = .fullScreen
将他们的 presentationStyle 更新为 .overFullScreen
后,故障消失了。