2

I'm trying to add different toolbars to each of my tabs but they are not displayed. The app will mostly be used on a landscape iPad and I can add the toolbars to the TabView itself and they display but then I don't know how to pass the button press down the navigation stack to the individual views/view-models to be handled locally.

I've tried adding new NavigationViews (including .stack navigationViewStyles) but this still just adds another column to the view.

This is some barebones, working code:

import SwiftUI

@main
struct NavTabTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
            MasterView()
    }
}

struct MasterView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<20) { index in
                    NavigationLink(
                        destination: DetailView(index: index)
                            .navigationTitle("Row \(index)")
                    ) {
                        Text("\(index) th row")
                    }.tag(index)
                }
            }.navigationTitle(Text("Ratty"))
        }
    }
}

struct DetailView: View {
    var index: Int
    
    @State var selectedTab = 1
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Tab1(index: index).tabItem { Label("Tab1", systemImage: "list.dash") }
            Tab2(index: index).tabItem { Label("Tab2", systemImage: "aqi.medium") }
            Tab3(index: index).tabItem { Label("Tab3", systemImage: "move.3d") }
        }
    }
}

struct Tab1: View {    
    var index: Int
    
    var body: some View {
        Text("This is \(index) in tab 1")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Bingo") { print("Bingo") }
                }
            }
    }
}

struct Tab2: View {
    var index: Int
    
    var body: some View {
        Text("This is \(index) in tab 2")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Bongo") { print("Bongo") }
                }
            }
    }
}

struct Tab3: View {
    var index: Int
    
    var body: some View {
        Text("This is \(index) in tab 3")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Banjo") { print("Banjo") }
                }
            }
    }
}

I'm starting to wonder if this is even possible and whether it would be better to just implement my own view with buttons at the top of each tab.

EDIT:

enter image description here

Magnas
  • 3,832
  • 5
  • 33
  • 48
  • NavigationView should a child of TabView, not the other way as you have done. Is it important to navigate to DetailsView? You can present it and add a NavgationView for each of the tab-views? – mahan Jul 01 '21 at 11:54
  • @mahan - I've tried moving the NavigationView inside the List but my view just becomes nonsense. I don't understand where I can put it. Also, I'm not tied to a NavigationLink but how would I populate the Detail view when choosing something from the list? – Magnas Jul 01 '21 at 13:48

2 Answers2

1

You need to use only one level NavigationView. In other words, you should not nest NavigationViews. Have a look at this answer.

Only Back Button Visible on Custom Navigation Bar SwiftUI


  1. Use a NavgationView for the MasterView as you have used.
  2. Use a NavigationView for each of the Tab# veiws.
  3. Switch between MasterView and DetailsView.
  4. Use Button instead of NavigationLink in MasterView (You can customise it to look like a NavigationLink)
  5. Use a custom back button in each of the Tab# veiws.

This controls which one of MasterView and DetailsView should be shown.

class BaseViewModel: ObservableObject {
    @Published var userFlow: UserFlow = .masterView
    
    init(){
        userFlow = .masterView
    }
    
    enum UserFlow {
        case masterView, detailsView
    }
}

This view to be used either in ContentView or instead of it. When you are in MasterView and click on one of the list row, set appState.userFlow = .detailView. When you click the back buttons, set appState.userFlow = .masterView.

Doing so, you switch between the two views and one NavigationView is shown at a time.

As of now, it does not have animation. Use if you wish so

https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions

struct BaseView: View {
    
    @EnvironmentObject var appState: BaseViewModel
    
    @State var index: Int = 0
    
    var body: some View {
        Group {
            switch appState.userFlow {
            case .masterView:
                MasterView(index: $index)
            default:
                DetailView(index: index)
            }
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}


Complete code

struct ContentView: View {
    var body: some View {
        BaseView().environmentObject(BaseViewModel())
    }
}


class BaseViewModel: ObservableObject {
    @Published var userFlow: UserFlow = .masterView
    
    init(){
        userFlow = .masterView
    }
    
    enum UserFlow {
        case masterView, detailsView
    }
}


struct BaseView: View {
    
    @EnvironmentObject var appState: BaseViewModel
    
    @State var index: Int = 0
    
    var body: some View {
        Group {
            switch appState.userFlow {
            case .masterView:
                MasterView(index: $index)
            default:
                DetailView(index: index)
            }
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}


struct MasterView: View {
    @EnvironmentObject var appState: BaseViewModel
    
    @Binding var index: Int
    
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<20) { index in
                    Button(action: {
                        appState.userFlow = .detailsView
                        $index.wrappedValue = index
                    }, label: {
                        HStack {
                            Text("\(index) th row")
                            Spacer()
                            Image(systemName: "greaterthan")
                        }
                    })
                    .tag(index)
                }
            }.navigationTitle(Text("Ratty"))
        }
    }
}

struct DetailView: View {
    var index: Int
    
    @State var selectedTab = 1
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Tab1(index: index).tabItem { Label("Tab1", systemImage: "list.dash") }
            Tab2(index: index).tabItem { Label("Tab2", systemImage: "aqi.medium") }
            Tab3(index: index).tabItem { Label("Tab3", systemImage: "move.3d") }
        }
    }
}

struct Tab1: View {
    @EnvironmentObject var appState: BaseViewModel
    
    var index: Int
    
    var body: some View {
        NavigationView {
            Text("This is \(index) in tab 1")
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("back") { appState.userFlow = .masterView  }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Bingo") { print("Bingo") }
                    }
                }
        }
    }
}

struct Tab2: View {
    @EnvironmentObject var appState: BaseViewModel
    
    var index: Int
    
    var body: some View {
        NavigationView {
            Text("This is \(index) in tab 2")
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("back") { appState.userFlow = .masterView  }
                    }
                    ToolbarItem(placement: .primaryAction) {
                        Button("Bongo") { print("Bongo") }
                    }
                }
        }
    }
}

struct Tab3: View {
    @EnvironmentObject var appState: BaseViewModel
    var index: Int
    
    var body: some View {
        NavigationView {
            Text("This is \(index) in tab 3")
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button("back") { appState.userFlow = .masterView  }
                    }
                    ToolbarItem(placement: .primaryAction) {
                        Button("Banjo") { print("Banjo") }
                    }
                }
        }
    }
}


I have customised xtwistedx 's solution.

mahan
  • 12,366
  • 5
  • 48
  • 83
  • Thanks! This looks promising and I'll work through it tomorrow. The different toolbars, though, are appearing in the master(List) view and not the individual detail/tab views. I'll see if I can use your code to achieve this and will credit your answer then. – Magnas Jul 01 '21 at 15:59
  • No, the toolbars are attatched to the tab-views. @Magnas – mahan Jul 01 '21 at 17:04
  • Yes, the toolbar is attached to the tab-views in the code, but the toolbar is attached to the master(left) view, I want the different toolbars to be attached to the detail(right) views. Please see my edit to my question. – Magnas Jul 01 '21 at 19:32
  • @Magnas Do the tab-views have navigation items (title, buttons, etc)? – mahan Jul 02 '21 at 08:40
0

Not sure if this will help but it does go over some interesting concepts with the toolbar in the nav view.

link: Stewart Lynch

cbear84
  • 486
  • 4
  • 13
  • Thanks for that. Interesting stuff but I think my problem is related to the specific way that TabViews handle/inherit NavigationViews. – Magnas Jul 01 '21 at 11:59