Reputation: 187
I'm encountering an odd issue with my UITabController
where my selected tab in the tab bar is out of sync with the tab displayed. Essentially I am building an iOS app in SwiftUI with navigation built in UIKit for customizability. The root view of the app is a UITabController
, which contains multiple UINavigationControllers
that manage navigation between UIHostingControllers
. In one of my pages that I navigate to in the navigation controllers, I am hiding the navigation bar on appear and showing it again when navigating back. However, changing tabs after doing this navigation does not work. When pressing a tab bar item, it does appear to become selected, but the actual screen shown does not change. Then, I can change tabs but they are offset compared to the icons (ie, switches to the wrong tabs). They are also no longer interactive. The UI Hierarchy debugger shows that both of the navigation controllers are still there, which probably should not be the case. The bug stops if I don't hide the navigation bar on that specific page.
Code to reproduce: (note, for this to work set "Application Scene Manifest (Generation)" to NO in build settings)
import SwiftUI
import UIKit
struct A: ViewControllable {
var holder = NavStackHolder()
var body: some View {
VStack {
Spacer()
Button("Press me") { holder.push(C()) }
Spacer()
}
.onAppear {
holder.showNavigationBar()
holder.viewController?.navigationItem.title = "A"
}
}
}
struct B: ViewControllable {
var holder = NavStackHolder()
var body: some View {
VStack {
Spacer()
Text("Some other tab")
Spacer()
}
.onAppear {
holder.showNavigationBar()
holder.viewController?.navigationItem.title = "B"
}
}
}
struct C: ViewControllable {
var holder = NavStackHolder()
var body: some View {
Text("I'm causing issues")
.onAppear { holder.hideNavigationBar() }
}
}
public class NavStackHolder {
public weak var viewController: UIViewController?
public weak var navigationController: CustomNavigationController? {
guard let navigationController = viewController?.navigationController else { return nil }
return navigationController as? CustomNavigationController
}
public func hideNavigationBar() {
navigationController?.setNavigationBarHidden(true, animated: true)
}
public func showNavigationBar() {
navigationController?.setNavigationBarHidden(false, animated: true)
}
public func push(_ viewControllable: any ViewControllable) {
navigationController?.pushViewController(viewControllable.viewController, animated: true)
}
public init() {}
}
public class CustomNavigationController: UINavigationController {
public override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if viewControllers.count >= 1 { viewController.hidesBottomBarWhenPushed = true }
super.pushViewController(viewController, animated: animated)
}
}
extension UINavigationController: @retroactive UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
public protocol ViewControllable: View {
var holder: NavStackHolder { get set }
func loadView()
func viewOnAppear(viewController: UIViewController)
}
extension ViewControllable {
var viewController: UIViewController {
let viewController = HostingController(rootView: self)
self.holder.viewController = viewController
return viewController
}
func loadView() {}
func viewOnAppear(viewController: UIViewController) {}
}
class HostingController<ContentView>: UIHostingController<ContentView> where ContentView: ViewControllable {
override func loadView() {
super.loadView()
self.rootView.loadView()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.rootView.viewOnAppear(viewController: self)
}
}
class TabController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let a = CustomNavigationController(rootViewController: A().viewController)
a.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
let b = CustomNavigationController(rootViewController: B().viewController)
b.tabBarItem = UITabBarItem(tabBarSystemItem: .more, tag: 1)
viewControllers = [a, b]
}
}
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = TabController()
window?.makeKeyAndVisible()
return true
}
}
NOTE: Only happens on real devices (not simulator). I am running iOS 18.1.1.
Upvotes: 0
Views: 35