Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Navigation + Tabview + Sheet broken in iOS 15

It looks like Navigation + TabView + Sheet is broken in iOS 15.

When I do this: ContentView -> DetailView -> Bottom Sheet

When the bottom sheet comes up, the Detail view is automatically popped off the stack: https://www.youtube.com/watch?v=gguLptAx0l4

I expect the Detail view to stay there even when the bottom sheet appears. Does anyone have any idea on why this happens and how to fix it?

Here is my sample code:

import Combine
import SwiftUI
import RealmSwift

struct ContentView: View {

    var body: some View {
        NavigationView {
            TabView {
                TabItemView(num: 1)
                    .tabItem {
                        Text("One")
                    }
                TabItemView(num: 2)
                    .tabItem {
                        Text("Two")
                    }
            }
        }
    }
}

struct TabItemView: View {

    private let num: Int

    init(num: Int) {
        self.num = num
    }

    var body: some View {
        NavigationLink(destination: DetailView(text: "Detail View \(num)")) {
            Text("Go to Detail View")
        }
    }
}

struct DetailView: View {

    @State private var showingSheet = false

    private let text: String

    init(text: String) {
        self.text = text
    }

    var body: some View {
        Button("Open Sheet") {
            showingSheet.toggle()
        }.sheet(isPresented: $showingSheet) {
            Text("Sheet Text")
        }
    }
}

This works on iOS 14 btw

UPDATE 1:

Tried @Sebastian's suggestion of putting NavigationView inside of TabView. While this fixed the nav bug, it fundamentally changed the behavior (I don't want to show the tabs in DetailView).

Also tried his suggestion of using Introspect to set navigationController.hidesBottomBarWhenPushed = true on the NavigationLink destination, but that didn't do anything:

struct ContentView: View {
    
    var body: some View {
        TabView {
            NavigationView {
                TabItemView(num: 1)
            }.tabItem {
                Text("One")
            }
            NavigationView {
                TabItemView(num: 2)
            }.tabItem {
                Text("Two")
            }
        }
    }
}

struct TabItemView: View {
    
    private let num: Int
    
    init(num: Int) {
        self.num = num
    }
    
    var body: some View {
        NavigationLink(destination: DetailView(text: "Detail View \(num)").introspectNavigationController { navigationController in
            navigationController.hidesBottomBarWhenPushed = true
        }) {
            Text("Go to Detail View")
        }
    }
}

struct DetailView: View {
    
    @State private var showingSheet = false
    
    private let text: String
    
    init(text: String) {
        self.text = text
    }
    
    var body: some View {
        Button("Open Sheet") {
            showingSheet.toggle()
        }.sheet(isPresented: $showingSheet) {
            Text("Sheet Text")
        }
    }
}
like image 498
skywalkerdude Avatar asked Nov 07 '22 00:11

skywalkerdude


1 Answers

You need to flip how you nest TabView & NavigationView. Instead of nesting several TabView views inside a NavigationView, use the TabView as the parent component, with a NavigationView for each tab.

This is how the updated ContentView would look like:

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationView {
                TabItemView(num: 1)
            }
            .tabItem {
                Text("One")
            }
            
            NavigationView {
                TabItemView(num: 2)
            }
            .tabItem {
                Text("Two")
            }
        }
    }
}

This makes sense and is more correct: The tabs should always be visible, but you want to show a different navigation stack with different content in each tab.

That it worked previously doesn't make it more correct - SwiftUI probably just changed its mind on dealing with unexpected situations. That, and the lack of error messages in these situations, is the downside of using a framework that tries to render anything you throw at it!


If the goal is specifically to hide the tabs when pushing a new view on a NavigationView (e.g., when tapping on a conversation in a messaging app), you have to use a different solution. Apple added the UIViewController.hidesBottomBarWhenPushed property to UIKit to support this specific use case.

This property is set on the UIViewController that, when presented, should not show a toolbar. In other words: Not the UINavigationController or the UITabBarController, but the child UIViewController that you push onto the UINavigationController.

This property is not supported in SwiftUI natively. You could set it using SwiftUI-Introspect, or simply write the navigation structure of your application using UIKit and write the views inside in SwiftUI, linking them using UIHostingViewController.

like image 110
Sebastian Avatar answered Dec 01 '22 07:12

Sebastian