NavigationLink on macOS not opening in the same View - swift

I'm currently building a macOS App with SwiftUI (no Catalyst) that is supposed to have a Sidebar and a single View to the right of it.
NavigationView {
List {
...
}
.listStyle(SidebarListStyle())
HomeView()
}
My Home View has a NavigationLink inside of it, pointing to another DetailView.
struct HomeView: View {
var body: some View {
NavigationLink("DetailView", destination: Text("This is the DV"))
}
}
This NavigationLink always appears as disabled and only works when I add another Column to the NavigationView. I don't want another Column, but rather the NavigationLink replacing the HomeView with the DetailView.
Is there any way to achieve this?

So apparently SwiftUI 2 does not support pushing views on to the Navigation Stack on macOS yet. However, I found this package that helps to resolve the issue:
github.com/lbrndnr/StackNavigationView

NavigationLinks always open a detail view from the current "column" of the navigation view, so if your navigation link is rendered as a child view of HomeView then it will try to open as a detail view of HomeView (the missing third column). If you want a view to replace the HomeView the intended way then the user should select something from the List. If you're trying to programmatically select a NavigationLink that already exists in your list then you should do so by changing state within the HomeView of a variable that is bound to the selected item of the list or the selection of the NavigationLink within the list. Here's a complete working example that does both within the ContentView.swift of a new MacOS SwiftUI Xcode project
struct HomeView: View {
#Binding var selectedItem: String?
var body: some View {
Button("Hello") {
selectedItem = "Thing2"
}
}
}
struct ContentView: View {
#State private var selectedItem: String?
private let items = ["Thing1", "Thing2"]
var body: some View {
NavigationView {
List(selection: $selectedItem) {
ForEach(items, id: \.self) {item in
NavigationLink(
destination: Text(item),
tag: item,
selection: $selectedItem
) {
Text(item)
}
}
}
HomeView(selectedItem: $selectedItem)
}
}
}
Here is the application displaying the HomeView
And here is the application displaying the selected thing after pressing the button
If you don't want to show anything selected in the list after navigating the view, but still want to use a navigation link to maintain the integrity of the navigation within the NavigationView then you could technically put a hidden navigation link in the list
NavigationView {
List(selection: $selectedItem) {
ForEach(items, id: \.self) {item in
NavigationLink(
destination: Text(item),
tag: item,
selection: $selectedItem
) {
Text(item)
}
}
NavigationLink(
destination: Text("Sneaky"),
tag: "Sneaky",
selection: $selectedItem
) {
Text("")
}.hidden()
}
HomeView(selectedItem: $selectedItem)
}
In that case make sure your button also selects the sneaky destination link
Button("Hello") {
selectedItem = "Sneaky"
}

Related

SwiftUI: List inside TabView inside NavigationView breaks animation

I want to have a TabView inside a NavigationView. The reason is that I am showing a List inside the TabView. When a user taps on an item in the list, the list uses a NavigationLink to show a detailed screen.
The problem is that the navigationBarTitle is now broken. It does not animate when the user scrolls through the items (sometimes). I don't want to wrap my NavigationView inside the TabView, since that will always show the TabView. I don't want it.
This is the reproduction code. If you run this in the simulator, the animation will break when switch a few times between the tabs and you scroll through the list. You will see that the navigation bar will remain where it is, without the animation.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TestView()
.tabItem {
Image(systemName: "person.3")
}
TestView()
.tabItem {
Image(systemName: "person.2")
}
}
.navigationTitle("Test")
.navigationViewStyle(.stack)
}
}
}
struct TestView: View {
var body: some View {
List {
Text("test")
}
.listStyle(.plain)
}
}

SwiftUI NavigationView adding extra view onto stack when sheet is dismissed

I have a List with ForEach using a NavigationLink that when tapped displays a detail view. The DetailsView includes a sheet to save the detail information into an array. After the save, the sheet is dismissed but an additional DetailsView is put on the navigation stack, so that I need to tap the back link twice to get back to the listing.
I'm likely doing something incorrect as I'm relatively new to swiftui, but can't determine what.
Three things of interest:
In the ListView, I use .navigationViewStyle(StackNavigationViewStyle()). When removed, the issue goes away but the iPad gets messy for the ListView.
I'm using insert(at: 0) to add data in my array because I want the most recent data at the top of the listing. If I use append instead, the issue does go away. Wanting the most recently saved item at the top of the list, I add a sort, however sorting causes the duplicate issue to reappear.
The issue only seems to occur when selecting the first item created in the list (the last in the array) and then saving a new item into the array.
steps:
click Tap Here First, then tap SAVE, enter a name then click Save.
click tab bar item Saved.
click on the list item from step 1 in the Saved Items listing (nav bar should show "< Saved Items").
click SAVE, enter another name then click Save. At this point, the duplicate view appears with "< Back" as the leading nav bar item, clicking it takes you to the original detail view, then clicking "< Saved Items" takes you to the list view.
What am I doing wrong or what should I be doing better?
xcode 12.4/iOS 14.1
Stripped down code to reproduce:
struct TestModel: Identifiable, Codable {
private(set) var id: UUID
var name: String
}
class AppData: ObservableObject {
#Published var testList = [TestModel]()
}
struct NewView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(item: TestModel(id: UUID(), name: ""))) {
Text("Tap here first")
}.navigationBarTitle("Main View", displayMode: .inline)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ListView: View {
#EnvironmentObject var appData: AppData
var body: some View {
NavigationView {
List {
ForEach(appData.testList) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
.navigationBarTitle("Saved Items", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle()) // remove this and issue goes away, but iPad gets "messy".
}
}
struct DetailView: View {
#State private var isSaveShowing = false
#State var item: TestModel
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 20) {
Text(item.name)
Button(action: {
isSaveShowing = true
}) {
Text("Save".uppercased())
}.sheet(isPresented: $isSaveShowing) {
SaveView(currentItem: item)
}
}
}
}
}
struct SaveView: View {
var currentItem: TestModel
#State private var name = ""
#EnvironmentObject var appData: AppData
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Name ".uppercased())
) {
TextField("Name (required)", text: $name)
}
}
.navigationBarItems(
trailing: Button(action: {
// appData.testList.append(TestModel(id: UUID(), name: name)) // using append instead of insert also resolves issue...
appData.testList.insert(TestModel(id: UUID(), name: name), at: 0)
presentationMode.wrappedValue.dismiss()
}) {
Text("Save")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView: View {
var appData = AppData()
var body: some View {
TabView {
NewView().tabItem {
Image(systemName: "rectangle.stack.badge.plus")
Text("Calculate")
}
ListView().tabItem {
Image(systemName: "tray.and.arrow.down")
Text("Saved")
}
}
.environmentObject(appData)
}
}
Apparently, this must have been a bug in SwiftUI.
Running the same code using Xcode 12.5 beta 3 with iOS 14.5 the issue no longer occurs.

SwiftUI Navigation through multiple screens

I'm a bit confused on how navigation works in SwiftUI. Does only the view starting the navigation need a NavigationView? I have one view with a NavigationView that has a NavigationLink to a second view. The second view then has a NavigationLink to a third and final view.
However, when my second view navigates to my third view, I get this message in the logs:
unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7f85d844bf90>.
I don't know if I'm handling navigation through multiple screens correctly and I'm getting some really odd behavior where pressing Next on my second screen takes me back to my first somehow...
//This is the link in my first view, my seconds link is the same except it does to my next step and the tag is different
NavigationLink(
destination: PasswordView(store: self.store),
tag: RegisterState.Step.password,
selection: .constant(store.value.step)
)
Navigation is a little bit tricky in SwiftUI, after creating one navigationview you don't need to create again in your 2nd or 3rd view. I am not sure how you creating it firstly. Here is an example how navigation is working.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondView()) {
Text("Show Second View")
}.navigationBarTitle("FirstView", displayMode: .inline)
}
}
}
}
struct SecondView: View {
var body: some View {
NavigationLink(destination: ThirdView()) {
Text("Show Third view")
}.navigationBarTitle("SecondView", displayMode: .inline)
}
}
struct ThirdView: View {
var body: some View {
Text("This is third view")
.navigationBarTitle("ThirdView", displayMode: .inline)
}
}

Removing large amounts of whitespace in a SwiftUI subview

Demonstration of whitespace problem
When I nest a NavigationView within a NavigationView, an enormous amount of whitespace separates the back button and the new navigation bar title. Is there something I'm doing wrong in terms of setting up my SwiftUI views?
import SwiftUI
struct Dashboard: View {
#EnvironmentObject var user: User
let courses = Course.exampleCourses()
var body: some View {
NavigationView {
List(courses) { course in
NavigationLink(destination: CourseView(course: course)) {
Text(course.name)
}
}.navigationBarTitle("Welcome, \(user.first)!")
}
}
}
import SwiftUI
struct CourseView: View {
// #ObservedObject allows us to update views whenever values in course change
#ObservedObject var course: Course
#EnvironmentObject var user: User
var body: some View {
NavigationView {
List {
NavigationLink(destination: WritingPromptView(prompt: "What is your course goal, \(user.first)?", explanationText: "This is the answer", textLocation: self.$course.goal)) {
Text("Course Goal")
}
NavigationLink(destination: NotepadView(parent: self.course)) {
Text("Notepad")
}
NavigationLink(destination: WritingPromptView(prompt: "<Reflection prompt goes here>", explanationText: "<How to reflect goes here>", textLocation: self.$course.reflection)) {
Text("Reflection")
}
}.navigationBarTitle(course.name)
}
}
}
It's a double NavigationBar. Just remove NavigationView from your CourseView. If you have Previews for CourseView, you will probably want to wrap it NavigationView there.

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