swiftuiswiftui-navigationlinkswiftui-tabviewios26

How to hide `TabViewBottomAccessory` when drilling down?


I would like to conditionally render TabViewBottomAccessory because I don't want to show it when I am drilling down. When going to the child view I want to hide both tab view (which I achieved by .toolbarVisibility(.hidden, for: .tabBar)) and its accessory.

  1. @Environment(\.tabViewBottomAccessoryPlacement) doesn't have a setter
  2. another .tabViewBottomAccessory down the tree has no effect
  3. my custom @Environment(\.isTabViewBottomAccessoryShown) has no effect on it
#Preview("PreviewView") {
    PreviewView()
}

enum Destination {
    case child
}

struct PreviewView: View {

    @State var tab: Int = 0

    var body: some View {
        TabView(selection: $tab) {
            Tab(value: 0) {
                NavigationStack {
                    List {
                        NavigationLink("To Child", value: Destination.child)
                    }
                    .navigationDestination(for: Destination.self) {
                        switch $0 {
                        case .child:
                            ChildView()
                                // 1 
                                .environment(\.isTabViewBottomAccessoryShown, false)
                                .toolbarVisibility(.hidden, for: .tabBar)
                                // 2
                                .tabViewBottomAccessory {
                                    EmptyView()
                                }
                        }
                    }
                }
            } label: {
                Label("Tab 1", systemImage: "checkmark")
            }
        }
        .tabViewBottomAccessory {
            TabViewBottomAccessory()
        }
    }
}

struct TabViewBottomAccessory: View {

    @Environment(\.tabViewBottomAccessoryPlacement) var placement
    // 3
    @Environment(\.isTabViewBottomAccessoryShown) var isTabViewBottomAccessoryShown

    var body: some View {
        Text("'\(placement)' '\(isTabViewBottomAccessoryShown)'")
    }
}

struct ChildView: View {

    var body: some View {
        List {
            Text("Child")
        }
    }
}


//

public struct TabViewBottomAccessoryVisibilityKey: EnvironmentKey {
    public static let defaultValue: Bool = true
}

extension EnvironmentValues {
    public var isTabViewBottomAccessoryShown: Bool {
        get { self[TabViewBottomAccessoryVisibilityKey.self] }
        set { self[TabViewBottomAccessoryVisibilityKey.self] = newValue }
    }
}


Solution

  • Environment values don't work because the tab bar bottom accessory is not a descendent of the navigation destination. Environment values are only propagated down the hierarchy.

    The tab bar bottom accessory is actually a cousin of the navigation destination - they share a common ancestor (the tab view) but one is not a parent of another. I would use a PreferenceKey to propagate the "preference" of whether they want the accessory view to be hidden, to PreviewView.

    struct ShouldHideBottomAccessory: PreferenceKey {
        static let defaultValue = false
        
        static func reduce(value: inout Bool, nextValue: () -> Bool) {
            value = value || nextValue()
        }
    }
    
    enum Destination {
        case child
    }
    
    struct PreviewView: View {
    
        @State private var tab: Int = 0
        @State private var shouldHide = false
    
        var body: some View {
            TabView(selection: $tab) {
                Tab(value: 0) {
                    NavigationStack {
                        List {
                            NavigationLink("To Child", value: Destination.child)
                        }
                        .navigationDestination(for: Destination.self) {
                            switch $0 {
                            case .child:
                                ChildView()
                            }
                        }
                    }
                } label: {
                    Label("Tab 1", systemImage: "checkmark")
                }
            }
            .onPreferenceChange(ShouldHideBottomAccessory.self) {
                shouldHide = $0
            }
            .tabViewBottomAccessory {
                if !shouldHide {
                    TabViewBottomAccessory()
                }
            }
        }
    }
    
    struct ChildView: View {
    
        var body: some View {
            List {
                Text("Child")
            }
            .preference(key: ShouldHideBottomAccessory.self, value: true)
        }
    }
    
    
    struct TabViewBottomAccessory: View {
        var body: some View {
            Text("Accessory")
        }
    }
    

    Note that this doesn't work in beta 2 (the app crashes when you navigate back to the root), but it does work in beta 3.