61

I have an iOS app with UITabBarController on a master screen, navigating to a detail screen hiding the UITabBarController with setting hidesBottomBarWhenPushed = true.

When going back to the master screen the UITabBarController does a strange "jump" as shown on this GIF:

enter image description here

This happens only on iOS 12.1, not on 12.0 or 11.x.

Seems like an iOS 12.1 bug, because I noticed other apps like FB Messenger with this behavior, but I was wondering, is there some kind of workaround for it?

Igor Kulman
  • 15,893
  • 10
  • 51
  • 114

12 Answers12

64

In your UITabBarController, set isTranslucent = false

Lukas Würzburger
  • 6,207
  • 7
  • 35
  • 68
binhnguyen14
  • 680
  • 5
  • 3
9

Apple has now fixed that in iOS 12.1.1

Valerio
  • 513
  • 2
  • 14
  • is it only a bug from iOS 12 or it was existing before also? i experienced it in 12.1.0 and then updated my iPhone to 12.1.2 so bug is disappeared now. but i want to know should i restrict it from all previous iOS versions or only for 12.1.0's. thanx! – Serj Rubens Dec 29 '18 at 18:50
  • 1
    @SerjRubens the bug was introduced in iOS 12 and fixed in iOS 12.1.1. I don't understand what do you mean by restricting the bug from the previous iOS versions – Valerio Jan 02 '19 at 11:02
  • ah ok, thats what i wanna know, thanks :) p.s. restricting mb not a best word but i added condition like below to avoid this bug from previous versions: tabBar.isTranslucent = ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 12, minorVersion: 1, patchVersion: 1)). so now i can change it as far as you saying it was ok in versions before 12 :) – Serj Rubens Jan 03 '19 at 12:52
  • Actually, I'm using Xcode 10.1 with a iOS target of 10.0, and I'm seeing the exact same bug behavior of a tab bar jump. – Phontaine Judd Nov 20 '19 at 04:33
5

I guess it's Apple's bug But you can try this as a hot fix: just create a class for your tabBar with following code:

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({$0.isKind(of: UITabBarButtonClass)})
            tabBarItems.forEach({itemFrames.append($0.frame)})
        }

        if !itemFrames.isEmpty, !tabBarItems.isEmpty, itemFrames.count == items?.count {
            tabBarItems.enumerated().forEach({$0.element.frame = itemFrames[$0.offset]})
        }
    }
}
Egor
  • 59
  • 3
4

In my case (iOS 12.1.4), I found that this weird glitchy behaviour was triggered by modals being presented with the .modalPresentationStyle = .fullScreen

After updating their presentationStyle to .overFullScreen, the glitch went away.

Edouard Barbier
  • 1,575
  • 2
  • 17
  • 31
3

Here's a solution that can handle rotation and tab bar items being added or removed:

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: $0)).hasSuffix("Button")
            }
            if buttonFrames.count == buttons.count, size == bounds.size {
                zip(buttons, buttonFrames).forEach { $0.0.frame = $0.1 }
            } else {
                buttonFrames = buttons.map { $0.frame }
                size = bounds.size
            }
        }
    }
}
Nick Dowell
  • 1,896
  • 16
  • 17
2
import UIKit

extension UITabBar{

open override func layoutSubviews() {
    super.layoutSubviews()
    if let UITabBarButtonClass = NSClassFromString("UITabBarButton") as? NSObject.Type{
        let subItems = self.subviews.filter({return $0.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)
    }
}
1

there are two ways to fix this issue, Firstly, In your UITabBarController, set isTranslucent = false like:

[[UITabBar appearance] setTranslucent:NO];

sencondly, if the first solution does not fix your issur, try this way:

here is the Objective-C code

// .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

More information:https://github.com/ChenYilong/CYLTabBarController/commit/2c741c8bffd47763ad2fca198202946a2a63c4fc

ElonChan
  • 8,372
  • 9
  • 54
  • 83
1

You can override - (UIEdgeInsets)safeAreaInsets method for few iOS 12 subversions with this:

- (UIEdgeInsets)safeAreaInsets {
    UIEdgeInsets insets = [super safeAreaInsets];
    CGFloat h = CGRectGetHeight(self.frame);
    if (insets.bottom >= h) {
        insets.bottom = [self.window safeAreaInsets].bottom;
    }
    return insets;
}
Sound Blaster
  • 4,220
  • 1
  • 21
  • 27
0

Thanks for the idea of @ElonChan, I just changed the c inline function to OC static method, since I won't use this overrideImplementation too much. And also, this snippet was adjusted to iPhoneX now.

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
boog
  • 1,243
  • 1
  • 12
  • 19
0

here is the swift code

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()
    }

} more information https://github.com/tonySwiftDev/UITabbar-fixIOS12.1Bug

0

I was facing the exact same issue, where the app was architectured with one navigation controller per tab. The easiest non-hacky way that I found to fix this, was to place the UITabBarController inside a UINavigationController, and remove the individual UINavigationControllers.

Before:

                   -> UINavigationController -> UIViewController
                   -> UINavigationController -> UIViewController
UITabBarController -> UINavigationController -> UIViewController
                   -> UINavigationController -> UIViewController
                   -> UINavigationController -> UIViewController

After:

                                             -> UIViewController
                                             -> UIViewController
UINavigationController -> UITabBarController -> UIViewController
                                             -> UIViewController
                                             -> UIViewController

By using the outer UINavigationController, you don't need to hide the UITabBar when pushing a view controller onto the navigation stack.

Caveat:

The only issue I found so far, is that setting the title or right/left bar button items on each UIViewController does not have the same effect. To overcome this issue, I applied the changes via the UITabBarControllerDelegate when the visible UIViewController has changed.

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
}

Note that I have added a preconditionFailure to catch any case when the navigation architecture has been modified

vfn
  • 5,907
  • 2
  • 31
  • 41
-1

If you still want to keep your tab bar translucent you need to subclass from UITabBar and override property 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
    }
} 

}

The idea is to not allow system to set zero insets, so tab bar won't jump.

Artem
  • 341
  • 3
  • 11