9

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")
        }
    }
}
skywalkerdude
  • 117
  • 1
  • 8
  • 1
    This is trickier than I thought! I’ve tried saving the NavigationLink state, saving the TabView selection state, and moving the sheet up to the TabView or NavigationView level. No luck! – Adam Sep 26 '21 at 19:22
  • Same here, and there is not real fix around it, even whit the answer below, works but if we use fullScreenCover, or sheet, the navigationLink pops to the previews View. – finalpets Oct 01 '21 at 04:11
  • @skywalkerdude Did you find a solution? – Lukas Dec 30 '21 at 17:24
  • @Lukas I ended up just coding my own tabview to get around this issue – skywalkerdude Jan 04 '22 at 23:29
  • @skywalkerdude could you share your solution? – SYL Jun 10 '22 at 10:01
  • @SYL it's a little difficult to share because I did it over a year ago. But the gist is that I didn't use the TabView provided by SwiftUI, but implemented my own using SwiftUI components and GeometryReaders. – skywalkerdude Jun 10 '22 at 17:41
  • @skywalkerdude I tried few different custom TabView and it's not work for me – SYL Jun 12 '22 at 08:38
  • Can you create a new question with your code, and I can try to take a look and help you? – skywalkerdude Jun 17 '22 at 18:59

1 Answers1

1

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.

Sebastian
  • 1,419
  • 1
  • 16
  • 24
  • Interesting. Why is it incorrect to nest a TabView inside of NavigationViews? Wouldn't it be a product/design decision to have tabs always visible or to have a screen come up and cover the tab? Instagram, for example, has the 5 tabs on the bottom, but if you hit messages on the upper right, the messages view completely covers the tabs. Is that an antipattern that I'm not aware of? – skywalkerdude Sep 27 '21 at 22:40
  • The behaviour you mention is supported differently, by setting `targetViewController.hidesBottomBarWhenPushed = true`. (This flag will hide any bottom bar - both tab bars and toolbars, like in the Mail app.) – Sebastian Sep 28 '21 at 10:18
  • I should add: As far as I know, `hidesBottomBarWhenPushed` is not yet built into SwiftUI, so you'd have to use SwiftUI-Introspect or something like that to set that flag. – Sebastian Sep 28 '21 at 10:25
  • Could you explain how to use `hidesBottomBarWhenPushed = true`? I imported SwiftUI-introspect, but I'm unable to get the bottom bar to disappear. I put it on both the `NavigationView` and the `TabView`, both to no avail – skywalkerdude Sep 30 '21 at 04:11
  • `TabView{...}.introspectTabBarController{tabView in tabView.hidesBottomBarWhenPushed = true }` – skywalkerdude Sep 30 '21 at 04:12
  • `TabView{NavigationView{...}.tabItem{...}.introspectNavigationController { nav in nav.hidesBottomBarWhenPushed = true }}` – skywalkerdude Sep 30 '21 at 04:15
  • this works, but if you try use a fullscreenCover or Sheet, the NavigationLink will pop to the previews Screen? even with the old fix with ```EmptyView()``` NavigationLink, does'nt work anymore – finalpets Oct 01 '21 at 04:13
  • @skywalkerdude - You apply "hidesBottomBarWhenPushed" not to the UITabBarController (= TabBar) or the UINavigationController (= NavigationView), but the the UIViewController that you push. That would be the destination view of your NavigationLink. (I'm not very familiar with Introspect though, so I don't know exactly how it would work.) – Sebastian Oct 01 '21 at 08:22
  • @finalpets That sounds like a separate question that needs a code sample! :-) – Sebastian Oct 01 '21 at 08:22
  • @Sebastian I am still unable to make it work. I added my example with your suggestion up in the original question. Could you help me with a small code sample of it working? – skywalkerdude Nov 03 '21 at 05:01
  • Your example applies "hidesBottomBarWhenPushed" on the UINavigationController (`introspectNavigationController`) - but you need to do it on the UIViewController, using `introspectViewController`. When doing that, ensure that the view controller you receive isn't either a UITabBarController or UINavigationController, as they are both UIViewControllers, too. Just add a breakpoint to check what it is, to be safe. – Sebastian Nov 04 '21 at 09:11
  • Sorry, it's still not working. I tried to add it to `DetailView`, since that is " the UIViewController that I am pushing", and I also used `introspectViewController` and added both logging and breakpoints to ensure that `viewController` is not actually a `UITabBarController` or a `UINavigationController`. The behavior is exactly the same. My branch is here: https://github.com/skywalkerdude/navsample/blob/sebastian-suggestion/NavSample/ContentView.swift – skywalkerdude Nov 22 '21 at 09:19
  • Hm - if it works, I'd have expected it to work like that. It's possible SwiftUI does something that makes this not work at all. The best solution (one I've taken in similar "why does this not work?!" situations) is probably to have the tabs/navigation views written in UIKit, and use SwiftUI only for the views inside those views. That's going to be more work, but it will also work more predictably. (Unless somebody else knows any other tricks to solve this.) – Sebastian Nov 22 '21 at 15:01