UITabBar items jumping on back navigation on iOS 12.1

IosSwiftUitabbar

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

In your UITabBarController, set isTranslucent = false

Solution 2 - Ios

Apple has now fixed that in iOS 12.1.1

Solution 3 - Ios

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

Solution 4 - Ios

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.

Solution 5 - Ios

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

Solution 6 - Ios

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

Solution 7 - Ios

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

Solution 8 - Ios

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;
}

Solution 9 - Ios

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

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionIgor KulmanView Question on Stackoverflow
Solution 1 - Iosbinhnguyen14View Answer on Stackoverflow
Solution 2 - IosValerioView Answer on Stackoverflow
Solution 3 - IosEgorView Answer on Stackoverflow
Solution 4 - IosEdouard BarbierView Answer on Stackoverflow
Solution 5 - IosNick DowellView Answer on Stackoverflow
Solution 6 - Iosuser10589608View Answer on Stackoverflow
Solution 7 - IosChenYilongView Answer on Stackoverflow
Solution 8 - IosSound BlasterView Answer on Stackoverflow
Solution 9 - IosboogView Answer on Stackoverflow
Solution 10 - IostonySwiftDevView Answer on Stackoverflow
Solution 11 - IosvfnView Answer on Stackoverflow
Solution 12 - IosArtemView Answer on Stackoverflow