SwiftUI TabView with nested NavigationView - swift

to summarize my problem:
My application contains a TabView on the root level. The children of the TabView must be searchable (using the apple native .searchable(...) modifier. Additionally at least one of the views must be able to push a third view on the stack which hides the TabBar on the bottom.
The problem with that is that both approaches: Using one single NavigationView in the root view and using additional navigation views inside the child views did not work.
Using only one NavigationView:
TabBar gets overlayed when navigating to another acitivty ✔
Not searchable ❌
Using nested NavigationViews:
TabBar is always visible also in the full screen activity ❌
Desired title + search behavior ✔
Root View:
public enum TestNavItemId: String, CaseIterable, Equatable {
case activity1 = "activity1"
case activity2 = "activity2"
public static func == (lhs: NavItemId, rhs: NavItemId) -> Bool {
lhs.rawValue == rhs.rawValue
}
}
struct DemoView: View {
var body: some View {
NavigationView {
TabView {
Activity1()
.tabItem {
Image(systemName: "house.fill")
Text(LocalizedStringKey(TestNavItemId.activity1.rawValue))
}
.tag(TestNavItemId.activity1)
Text("Nothing to see here")
.tabItem {
Image(systemName: "fire")
Text(LocalizedStringKey(TestNavItemId.activity2.rawValue))
}
.tag(TestNavItemId.activity2)
}
}
}
}
Activity1:
struct Activity1: View {
#State private var query: String = ""
var body: some View {
NavigationView {
VStack {
Text("Content")
NavigationLink(destination: Text("Full Screen View")) {
Text("Open full screen view")
}
}
.searchable(text: $query)
}
}
}
Any help is much appreciated!

Related

Interacting with a confirmationDialog or alert is causing the parent view to pop

When you navigate and open the confirmation dialog. When you select Yes, No or Cancel the page that the app was on is dismissed and it takes you back to the form on the previous page.
We also found this happens with alerts too.
It's a simple enough app structure, top level tabs then a menu which links to sub pages.
Here is a quick demo of the bug:
We put together an example app that demonstrates this.
How can we prevent this from happening while also maintaining the app structure?
import SwiftUI
#main
struct testApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
TabView() {
Form {
NavigationLink(destination: SubPage()) {
Image(systemName: "clock")
Text("Sub Page")
}
// This is where more menu options would be
}
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
struct SubPage: View {
#State private var confirmDialogVisible = false
var body: some View {
VStack {
Button{
confirmDialogVisible = true
} label: {
Text("popup")
}
}
.confirmationDialog("Confirm?", isPresented: $confirmDialogVisible) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
}
We are using XCode 14.1
And running on iOS 16.1
I usually use ViewModifer to keep consistency between tabs. One modifier for the root of the tab and one for the children.
///Modifier that uses `ToolbarViewModifier` and includes a `NavigationView`
struct NavigationViewModifier: ViewModifier{
func body(content: Content) -> some View {
NavigationView{
content
.modifier(ToolbarViewModifier())
}
}
}
///`toolbar` that can be used by the root view of the navigation
///and the children of the navigation
struct ToolbarViewModifier: ViewModifier{
let title: String = "Company Name"
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
VStack{
Image(systemName: "sparkles")
Text(title)
}
}
}
}
}
Then the Views use it something like this.
import SwiftUI
struct CustomTabView: View {
var body: some View {
TabView{
ForEach(0..<4){ n in
CustomChildView(title: n.description)
//Each tab gets a `NavigationView` and the shared toolbar
.modifier(NavigationViewModifier())
.tabItem {
Text(n.description)
}
}
}
}
}
struct CustomChildView: View {
let title: String
#State private var showConfirmation: Bool = false
var body: some View {
VStack{
Text(title)
NavigationLink {
CustomChildView(title: "\(title) :: \(UUID().uuidString)")
} label: {
Text("open child")
}
Button("show confirmation") {
showConfirmation.toggle()
}
.confirmationDialog("Confirm?", isPresented: $showConfirmation) {
Button("Yes") {
print("yes")
}
Button("No", role: .destructive) {
print("no")
}
}
}
//Each child uses the shared toolbar
.modifier(ToolbarViewModifier())
}
}
I stuck with NavigationView since that is what you have in your code but
if we take into consideration the new NavigationStack the possibilities of these two modifiers become exponentially better.
You can include custom back buttons that only appear if the path is not empty, return to the root from anywhere, etc.
Apple says that
Tab bars use bar items to navigate between mutually exclusive panes of content in the same view
Make sure the tab bar is visible when people navigate to different areas in your app
https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars/
Having the NavigationView or NavigationStack on top goes against these guidelines and therefore are the source of endless bugs, Especially when you take into consideration iPadOS.
Simple solution would be to use navigationDestination.
struct testApp: App {
#State var goToSubPage = false
var body: some Scene {
WindowGroup {
NavigationStack {
TabView() {
Form {
VStack {
Image(systemName: "clock")
Text("Sub Page")
}
.onTapGesture {
goToSubPage = true
}
// This is where more menu options would be
}
.navigationDestination(isPresented: $goToSubPage, destination: {
SubPage()
})
.tag(1)
.tabItem {
Image(systemName: "square.grid.2x2")
Text("Tab 1")
}
// This is where more tab pages would be
}
}
}
}
}
I tested it and it won't popped off itself anymore.

Using a SwiftUI List Sidebar in a UISplitViewController

I am attempting to build my app's navigation such that I have a UISplitViewController (triple column style) with my views built with SwiftUI. My Primary Sidebar is currently quite simple:
struct PrimarySidebarView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
List(PrimarySidebarSelection.allCases, id: \.self, selection: $appModel.primarySidebarSelection) { selection in
Text(selection.rawValue)
}
.listStyle(SidebarListStyle())
.navigationBarItems(trailing: EditButton())
}
}
where PrimarySidebarSelection is an enum. I am planning to access the same AppModel environment object in my other sidebar, allowing me to change what is displayed in the Supplementary Sidebar, depending on the Primary Selection. I am using the new SwiftUI App life-cycle, rather than an AppDelegate.
I would like to know how to change the style of selection from this to the typical sidebar selection style that is used in SwiftUI's NavigationView. According to SwiftUI's List Documentation the selection is only available when the list is in edit mode (and the selection shows the circle next to each item, which I do not want, instead I want the row to highlight like how it does in NavigationView when working with NavigationLinks).
Thanks in advance.
enum PrimarySidebarSelection: String, CaseIterable {
case a,b,c,d,e,f,g
}
struct SharedSelection: View {
#StateObject var appModel: AppModel = AppModel()
var body: some View {
NavigationView{
PrimarySidebarView().environmentObject(appModel)
Text(appModel.primarySidebarSelection.rawValue)
}
}
}
class AppModel: ObservableObject {
#Published var primarySidebarSelection: PrimarySidebarSelection = .a
}
struct PrimarySidebarView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
List{
ForEach(PrimarySidebarSelection.allCases, id: \.self) { selection in
Button(action: {
appModel.primarySidebarSelection = selection
}, label: {
HStack{
Spacer()
Text(selection.rawValue)
.foregroundColor(selection == appModel.primarySidebarSelection ? .red : .blue)
Spacer()
}
}
)
.listRowBackground(selection == appModel.primarySidebarSelection ? Color(UIColor.tertiarySystemBackground) : Color(UIColor.secondarySystemBackground))
}
}
.listStyle(SidebarListStyle())
.navigationBarItems(trailing: EditButton())
}
}

SwiftUI: Dismiss View Within macOS NavigationView

As detailed here (on an iOS topic), the following code can be used to make a SwiftUI View dismiss itself:
#Environment(\.presentationMode) var presentationMode
// ...
presentationMode.wrappedValue.dismiss()
However, this approach doesn't work for a native (not Catalyst) macOS NavigationView setup (such as the below), where the selected view is displayed alongside the List.
Ideally, when any of these sub-views use the above, the list would go back to having nothing selected (like when it first launched); however, the dismiss function appears to do nothing: the view remains exactly the same.
Is this a bug, or expected macOS behaviour?
Is there another approach that can be used instead?
struct HelpView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination:
AboutAppView()
) {
Text("About this App")
}
NavigationLink(destination:
Text("Here’s a User Guide")
) {
Text("User Guide")
}
}
}
}
}
struct AboutAppView: View {
#Environment(\.presentationMode) var presentationMode
public var body: some View {
Button(action: {
self.dismissSelf()
}) {
Text("Dismiss Me!")
}
}
private func dismissSelf() {
presentationMode.wrappedValue.dismiss()
}
}
FYI: The real intent is for less direct scenarios (such as triggering from an Alert upon completion of a task); the button setup here is just for simplicity.
The solution here is simple. Do not use Navigation View where you need to dismiss the view.
Check the example given by Apple https://developer.apple.com/tutorials/swiftui/creating-a-macos-app
If you need dismissable view, there is 2 way.
Create a new modal window (This is more complicated)
Use sheet.
Following is implimenation fo sheet in macOS with SwiftUI
struct HelpView: View {
#State private var showModal = false
var body: some View {
NavigationView {
List {
NavigationLink(destination:
VStack {
Button("About"){ self.showModal.toggle() }
Text("Here’s a User Guide")
}
) {
Text("User Guide")
}
}
}
.sheet(isPresented: $showModal) {
AboutAppView(showModal: self.$showModal)
}
}
}
struct AboutAppView: View {
#Binding var showModal: Bool
public var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Dismiss Me!")
}
}
}
There is also a 3rd option to use ZStack to create a Modal Card in RootView and change opacity to show and hide with dynamic data.

Two UINavigationControllers after using NavigationLink in sheet

I have a modal sheet that is presented from my home view as such:
Button(action: {
...
}) {
...
}
.sheet(isPresented: ...) {
MySheetView()
}
In MySheetView, there is a NavigationView and a NavigationLink to push another view onto its view stack (while I'm on MySheetView screen and use the view inspector, there's only one UINavigationController associated with it which is what I expect).
However, as soon as I get to my next view that is presented from MySheetView using the NavigationLink, and I use the view hierarchy debugger, there are TWO UINavigationControllers on-top of each other. Note, this view does NOT have a NavigationView inside it, only MySheetView does.
Does anyone know what's going on here? I have a feeling this is causing some navigation bugs im experiencing. This can be easily reproduced in an example app with the same structure.
Ex:
// These are 3 separate SwiftUI files
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
Button(action: { self.isPresented = true }) {
Text("Press me")
}
.sheet(isPresented: $isPresented) {
ModalView()
}
}
}
}
struct ModalView: View {
var body: some View {
NavigationView {
NavigationLink(destination: FinalView()) {
Text("Go to final")
}
}
}
}
struct FinalView: View {
var body: some View {
Text("Hello, World!")
}
}
I don't observe the behaviour you described. Used Xcode 11.2. Probably you need to provide your code to find the reason.
Here is an example of using navigation views in main screen and sheet. (Note: removing navigation view in main screen does not affect one in sheet).
import SwiftUI
struct TestNavigationInSheet: View {
#State private var hasSheet = false
var body: some View {
NavigationView {
Button(action: {self.hasSheet = true }) {
Text("Show it")
}
.navigationBarTitle("Main")
.sheet(isPresented: $hasSheet) { self.sheetContent }
}
}
private var sheetContent: some View {
NavigationView {
VStack {
Text("Properties")
.navigationBarTitle("Sheet")
NavigationLink(destination: properties) {
Text("Go to Inspector")
}
}
}
}
private var properties: some View {
VStack {
Text("Inspector")
}
}
}
struct TestNavigationInSheet_Previews: PreviewProvider {
static var previews: some View {
TestNavigationInSheet()
}
}

How can I avoid nested Navigation Bars in SwiftUI?

Using SwiftUI, I've built a NavigationView that takes the user to another NavigationView, and finally, to a simple View. When I get to the last view, I can see two back buttons and a very large Navigation Bar.
I'd like to have a navigation structure similar to the iOS Settings app, where one navigation list takes to another and each of them have one back button that goes back to the previous screen.
Does anyone know how to solve this?
You should only have one NavigationView in your view hierarchy, as an ancestor of the menu view. You can then use NavigationLinks at any level of the hierarchy under that.
So, for example, your root view could be defined like this:
struct RootView: View {
var body: some View {
NavigationView {
MenuView()
.navigationBarItems(trailing: profileButton)
}
}
private var profileButton: some View {
Button(action: { }) {
Image(systemName: "person.crop.circle")
}
}
}
Then your menu view has NavigationLinks to the appropriate views:
struct MenuView: View {
var body: some View {
List {
link(icon: "calendar", label: "Appointments", destination: AppointmentListView())
link(icon: "list.bullet", label: "Work Order List", destination: WorkOrderListView())
link(icon: "rectangle.stack.person.crop", label: "Contacts", destination: ContactListView())
link(icon: "calendar", label: "My Calendar", destination: MyCalendarView())
}.navigationBarTitle(Text("Menu"), displayMode: .large)
}
private func link<Destination: View>(icon: String, label: String, destination: Destination) -> some View {
return NavigationLink(destination: destination) {
HStack {
Image(systemName: icon)
Text(label)
}
}
}
}
Your appointment list view also contains NavigationLinks to the appointment detail views:
struct AppointmentListView: View {
var body: some View {
List {
link(destination: AppointmentDetailView())
link(destination: AppointmentDetailView())
link(destination: AppointmentDetailView())
}.navigationBarTitle("Appointments")
}
private func link<Destination: View>(destination: Destination) -> some View {
NavigationLink(destination: destination) {
AppointmentView()
}
}
}
Result:
If you create the Menu View with NavigationLinks but don't declare it inside a NavigationView, you'll get the child view without NavigationBar.
Do not use NavigationView to wrap your list in the "AppointmentView"
e.g.
NavigationView{
List{
}
}
But only use List in the "AppointmentView"
List{
}
you can still use NavigationLink inside that List