54

I have built my app using new Xcode 9 beta for ios 11. I have found an issue with UITabBar where items are spread through the UITabBar and title is right aligned to the image. I have tried changing the code to get it to work but still not successful.

ios 10+

enter image description here

ios 11

enter image description here

I could change the position of title using tabBarItem.titlePositionAdjustment But that is not my requirement as it should automatically come bellow the image itself. I tried setting tabbar.itemPositioning to UITabBarItemPositioningCentered and also tried changing itemSpacing and width, but still did not work. Can someone help me understand why this happens and how to fix this? I want it to like ios 10+ version and images are taken from the left most corner of an iPad.

Keyur
  • 180
  • 1
  • 5
  • 20
Gihan
  • 2,476
  • 2
  • 27
  • 33
  • 13
    It's a feature, not a bug. – matt Aug 23 '17 at 23:45
  • 1
    This is not just issue with objective-c but in general with objective-c and swift application both. – Jeet Sep 24 '17 at 06:55
  • 1
    Feature? idk. More a case of Apple "improving" something without thinking through all of the consequences. – William Jockusch Dec 31 '17 at 18:20
  • What are consequences. It works like this only when there is enough space for that. Apple cares about everything... it is a feature. We should buy them bottle of wine for that;) More space can interact with user now. Space is now fully usable. – Bartłomiej Semańczyk Jan 04 '18 at 13:05

8 Answers8

47

I am maintaining a large iPad app written mostly in Objective-C that has survived several iOS releases. I ran into the situation where I needed the pre-iOS 11 tab bar appearance (with the icons above the titles instead of next to them) for a couple tab bars. My solution was to create a subclass of UITabBar that overrides the traitCollection method so that it always returns a horizontally-compact trait collection. This causes iOS 11 to display the titles below the icons for all of the tab bar buttons.

In order to use this, set the custom class of the tab bars in the storyboard to this new subclass and change any outlets in the code that point to the tab bars to be of this new type (don't forget to import the header file below).

The .h file is pretty much empty in this case:

//
//  MyTabBar.h
//

#import <UIKit/UIKit.h>

@interface MyTabBar : UITabBar

@end

Here is the .m file with the implementation of the traitCollection method:

//
//  MyTabBar.m
//

#import "MyTabBar.h"

@implementation MyTabBar

// In iOS 11, UITabBarItem's have the title to the right of the icon in horizontally regular environments
// (i.e. the iPad).  In order to keep the title below the icon, it was necessary to subclass UITabBar and override
// traitCollection to make it horizontally compact.

- (UITraitCollection *)traitCollection {
    return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}

@end
John C.
  • 486
  • 5
  • 2
  • 2
    I don't use storyboard/IB but I was able to get the exact same effect by subclassing UITabBarController instead of UITabBar and overriding the same traitCollection method. – massimobio Aug 31 '17 at 18:14
  • Not only! It also works with storyboard/IB that use subclasses of UITabBarController with this fix... – Daniele Ceglia Sep 07 '17 at 12:28
  • 2
    Actually scratch that. Subclassing UITabBarController is not recommended and in fact the navigation bar broke with iOS 11 GM when doing this. – massimobio Sep 19 '17 at 16:42
  • Any idea on how to programmatically use the UITabBar subclass in a stock UITabBarController? – massimobio Sep 19 '17 at 16:58
  • 1
    @massimobio make a storyboard and instantiate your UITabBarController in code. (Seriously.) – nolanw Sep 24 '17 at 16:08
  • 2
    Unfortunately doing that breaks the navigation bar appearance (UIBarButtonItems and the nav bar title): https://forums.developer.apple.com/thread/86013 – Niko Zarzani Oct 02 '17 at 11:24
  • 2
    @NikoZarzani It breaks the navigation bar appearance only if you subclass UITabBarController. In case you subclass UITabBar the navigation bar appearance left as it is. – Eugene Brusov Jan 10 '18 at 18:00
  • 3
    Sadly, with iOS 13 this started producing this warning `Class MyClass overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.` – zaitsman Sep 26 '19 at 05:58
  • Yes iOS 13 was giving me the same errors but if you use the solution below by @andy-c instead, most of those error messages disappear. That solution still spits out "[TraitCollection] Class CustomiPadUITabBar overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API." Whatever the "appropriate API" is? – Seoras Oct 09 '19 at 00:50
46

Based on John C's answer, here is the Swift 3 version that can be used programmatically without need for Storyboard or subclassing:

extension UITabBar {
    // Workaround for iOS 11's new UITabBar behavior where on iPad, the UITabBar inside
    // the Master view controller shows the UITabBarItem icon next to the text
    override open var traitCollection: UITraitCollection {
        if UIDevice.current.userInterfaceIdiom == .pad {
            return UITraitCollection(horizontalSizeClass: .compact)
        }
        return super.traitCollection
    }
}
massimobio
  • 808
  • 9
  • 11
  • 2
    This is not recommended, though I'm not surprised to hear that it works at the moment. If `UITabBar` also overrides `traitCollection`, and/or if there's another extension (or Objective-C category) on `UITabBar` somewhere that implements it, then the result is undefined. You're better off subclassing (annoying as that is). See https://stackoverflow.com/questions/5267034/category-conflicts for more info. – nolanw Sep 24 '17 at 16:07
  • @nolanw Is there a way to use a subclass if you're merely using a `UITabBarController`? Note: this answer does work in that case, too. – Dan Rosenstark Oct 22 '17 at 18:20
  • 1
    @DanRosenstark yep: use a storyboard. I have a storyboard in a project that has a plain `UITabBarController` whose only customization is setting its tab bar's class, and I load it in code. It's a pain in the butt, but it's safer than swizzling and much safer than hopping there's no category conflicts. – nolanw Oct 23 '17 at 18:43
  • @nolanw You can also use separate xib file with tab bar controller as a root view in that xib layout. Then load tab bar controller with `Bundle.main.loadNibNamed("MyTabBarController", owner: nil, options: [])?.first` – Eugene Brusov Jan 10 '18 at 18:04
  • causes crash in iOS 17, use answer from @Balázs Vincze – slavik Jun 22 '23 at 13:56
24

To avoid messing up any other traits is it not better to combine with the superclasses:

- (UITraitCollection *)traitCollection
{
  UITraitCollection *curr = [super traitCollection];
  UITraitCollection *compact = [UITraitCollection  traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];

  return [UITraitCollection traitCollectionWithTraitsFromCollections:@[curr, compact]];
}
Andy C
  • 341
  • 3
  • 3
  • This is indeed better! – nolanw Sep 24 '17 at 16:08
  • Awesome..this works for me. When I just had the Compact size class set for trait collection, it fixed my tab bar items, but had broken my navigation bar buttons. Thank you!! – Bharat Raichur Oct 17 '17 at 11:10
  • @BharatRaichur Subclass UITabBar not UITabBarController then override `- (UITraitCollection *)traitCollection{...}`. That won't brake your nav bar. – Eugene Brusov Jan 10 '18 at 18:06
  • 8
    iOS 13 was spitting out a large amount of debugs in the console about traits which this solution fixed. One error is still being report though "[TraitCollection] Class CustomiPadUITabBar overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.". Any ideas how to get rid of that one? Is there an "appropriate API" ? – Seoras Oct 09 '19 at 00:55
16

NOTE: This fix above seems to work quite nicely. Not sure how it will work in the future, but for now it's working quite well.


According to this WWDC talk, this is the new behavior for:

  • iPhone in landscape
  • iPad all the time

And if I'm reading correctly, this behavior cannot be changed:

We've got this brand new appearance for the tab bar, both in landscape and on iPad, where we show the icon and the text side by side. And in particular on iPhones the icon is smaller and the tab bar is smaller to give a bit more room vertically.

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
16

Swift 4 version with a subclass that avoids extension/category naming collision:

class TabBar: UITabBar {
  override var traitCollection: UITraitCollection {
    guard UIDevice.current.userInterfaceIdiom == .pad else {
      return super.traitCollection
    }

    return UITraitCollection(horizontalSizeClass: .compact)
  }
}

If you use Interface Builder and storyboards, you can select the tab bar view in your UITabBarController scene and change its class to TabBar in the Identity Inspector:

enter image description here

Max Desiatov
  • 5,087
  • 3
  • 48
  • 56
3

In Addition to John's answer:

If you have more than 4 Tab Bar Items and don't want the "More" Button you have to take a different Size Class. And if you want the original centred layout of items you have to add another method like so:

#import "PreIOS11TabBarController.h"

@interface PreIOS11TabBarController ()

@end

@implementation PreIOS11TabBarController

// In iOS 11, UITabBarItem's have the title to the right of the icon in horizontally regular environments
// (i.e. the iPad).  In order to keep the title below the icon, it was necessary to subclass UITabBar and override
// traitCollection to make it horizontally compact.

- (UITraitCollection *)traitCollection {
    return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
}


- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    self.tabBar.itemPositioning = UITabBarItemPositioningCentered;
}

@end
twofish
  • 354
  • 4
  • 16
2

@massimobio's answer is good, however it caused the large navigation bar title to disappear for me. Have not investiaged the issue further, but this seems to fix it:

override var traitCollection: UITraitCollection {
        guard UIDevice.current.userInterfaceIdiom == .pad else {
            return super.traitCollection
        }

        return UITraitCollection(traitsFrom: [super.traitCollection, UITraitCollection(horizontalSizeClass: .compact)])
    }
Balázs Vincze
  • 3,550
  • 5
  • 29
  • 60
2

Max Desiatov's answer works partially. It messes around with the light/dark mode.

Here is another solution which does not interfere with the main traitCollection, but works together.

class CompactTabBar: UITabBar {
    
    override var traitCollection: UITraitCollection {
        guard UIDevice.current.userInterfaceIdiom == .pad else {
            return super.traitCollection
        }
        
        let compactTrait = UITraitCollection(horizontalSizeClass: .compact)
        let tabTrait = UITraitCollection(traitsFrom: [super.traitCollection, compactTrait])
        
        return tabTrait
    }
}

Just assign this class to the UITabBar object from the UITabBarController in the UIStoryboard.

Starsky
  • 1,829
  • 18
  • 25