Skip to content

Disable New UITabBarController Style in iPadOS 18

Problem Statement

iPadOS 18 introduces a new floating tab bar design that moves tabs to the top of the screen. While some may welcome this update, many existing apps need to maintain the classic bottom-aligned tab bar for UX consistency, design compatibility, or existing customizations. This article explains how to revert to the pre-iPadOS 18 tab bar appearance across SwiftUI and UIKit.

Override Horizontal Size Class

The most reliable method is overriding the horizontal size class to .compact or .unspecified. This preserves Apple's official APIs while achieving the desired appearance:

UIKit Implementation:

swift
class CustomTabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        if #available(iOS 18.0, *), UIDevice.current.userInterfaceIdiom == .pad {
            traitOverrides.horizontalSizeClass = .compact // Use .unspecified to show all tabs
        }
    }
}

SwiftUI Implementation:

swift
TabView {
    // Your tabs here
}
.environment(\.horizontalSizeClass, .compact)

Key Differences

  • .compact: Bottom-aligned tabs with "More" menu (ideal for ≤ 5 tabs)
  • .unspecified: Bottom-aligned tabs without truncation (ideal for > 5 tabs)

Prevent Side Effects in Child Views

Overriding the size class affects child view controllers. Restore the original traits in child content:

UIKit:

swift
class ChildViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        if #available(iOS 17.0, *) {
            traitOverrides.horizontalSizeClass = .regular
        }
    }
}

SwiftUI:

swift
struct ChildView: View {
    @Environment(\.horizontalSizeClass) private var originalSizeClass
    
    var body: some View {
        ContentView()
            .environment(\.horizontalSizeClass, originalSizeClass)
    }
}

macOS Compatibility

For iPad apps running on macOS Sequoia:

swift
class MacCompatibleTabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        if #available(iOS 18.0, *), ProcessInfo.processInfo.isiOSAppOnMac {
            mode = .tabSidebar
            sidebar.isHidden = true
            traitOverrides.horizontalSizeClass = .compact
        }
    }
}

Alternative Approaches

UserDefaults Method (Use with Caution)

swift
@main
struct MyApp: App {
    init() {
        UserDefaults(suiteName: "com.apple.UIKit")?.set(false, forKey: "UseFloatingTabBar")
    }
}

Compatibility Risks

  • Must be set before UI initialization
  • Uses private APIs (may cause App Store rejection)
  • Affects all UIKit components in your app

Rotation Workaround

Fix layout issues during device rotation:

swift
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    if #available(iOS 18.0, *) {
        traitOverrides.horizontalSizeClass = .regular
        traitOverrides.horizontalSizeClass = .compact
    }
}

Comprehensive Solution

This combines all best practices with side-effect prevention:

swift
class OptimizedTabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        configureForiPadOS18()
    }
    
    private func configureForiPadOS18() {
        guard #available(iOS 18.0, *),
              UIDevice.current.userInterfaceIdiom == .pad else { return }
        
        traitOverrides.horizontalSizeClass = .compact
        
        // Restore original traits for children
        viewControllers?.forEach {
            $0.traitOverrides.horizontalSizeClass = UIApplication.shared
                .firstKeyWindow?
                .traitCollection.horizontalSizeClass
        }
        
        // macOS-specific fixes
        if ProcessInfo.processInfo.isiOSAppOnMac {
            mode = .tabSidebar
            sidebar.isHidden = true
        }
    }
}

// Helper extension
extension UIApplication {
    var firstKeyWindow: UIWindow? {
        connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first { $0.windows.count > 0 }
            .flatMap { $0.windows.first(where: \.isKeyWindow) }
    }
}

Important Considerations

  1. Xcode 16+ Compatibility:
    Add runtime checks for newer SDKs:

    swift
    #if compiler(>=6.0)
    if #available(iOS 18.0, *) { /* ... */ }
    #endif
  2. Trait Inheritance:

    Always restore original traits in child view controllers to avoid unexpected layout issues

  3. Future Updates:
    These solutions rely on current UIKit behavior. Test with each new iOS beta and be prepared to adjust implementations as Apple's APIs evolve.

By implementing these solutions, your app will maintain the classic tab bar appearance while remaining compatible with iPadOS 18+ and future OS versions.