SwiftUI - Navigation animations not working when multiple links are in same view - swift

In my project I have a root view which contains several NavigationLinks. The links work, however the animation only works on one of them. I've created a very simple example project to highlight the issue. So I have a content view like this:
struct ContentView: View {
#StateObject var viewModel: ContentViewModel
var body: some View {
NavigationView {
VStack {
Text("Content view")
Button {
viewModel.goToTapped(.details)
} label: {
Text("To Details")
}
navigationLinks
}
.padding()
}
}
private var navigationLinks: some View {
HStack {
NavigationLink(
destination: DetailsView(viewModel: viewModel)
.navigationBarHidden(true),
tag: ContentViewModel.ViewState.details,
selection: $viewModel.viewState
) { EmptyView() }
NavigationLink(
destination: ContentView(viewModel: .init())
.navigationBarHidden(true),
tag: ContentViewModel.ViewState.content,
selection: $viewModel.viewState
) { EmptyView() }
NavigationLink(
destination: PaymentView(viewModel: viewModel)
.navigationBarHidden(true),
tag: ContentViewModel.ViewState.payment,
selection: $viewModel.viewState
) { EmptyView() }
NavigationLink(
destination: CardView(viewModel: viewModel)
.navigationBarHidden(true),
tag: ContentViewModel.ViewState.card,
selection: $viewModel.viewState
) { EmptyView() }
}
}
}
The associate viewModel:
class ContentViewModel: ObservableObject {
enum ViewState {
case content
case details
case payment
case card
}
#Published var viewState: ViewState?
func goToTapped(_ viewState: ViewState) {
self.viewState = viewState
}
}
DetailsView:
struct DetailsView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Details View")
Button {
viewModel.goToTapped(.payment)
} label: {
Text("To Payment")
}
Button {
viewModel.goToTapped(.content)
} label: {
Text("Back to Content")
}
}
}
}
PaymentView:
struct PaymentView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
NavigationView {
VStack {
Text("Payment View")
Button {
viewModel.goToTapped(.card)
} label: {
Text("To Card")
}
Button {
viewModel.goToTapped(.details)
} label: {
Text("Back To Details")
}
}
}
}
}
CardView:
struct CardView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
Text("Card view")
Button {
viewModel.goToTapped(.payment)
} label: {
Text("Back To Payment")
}
}
}
And here is the result:
So it seems that when the NavigationLink is declared within the same view as the transition trigger, the animation works fine. However, once we have navigated to a new view, whilst the navigation works, the animation does not.
I tried moving the NavigationLinks into the individual views, but of course because we are listening to the same viewState publisher for all views, this does not work because as soon as the viewState is changes, the the navigation stack dismisses and we go back to the root view (ContentView).
How can declare multiple NavigationLinks like this within a root view without impacting on the navigation animation?

UPDATE
So in iOS 16 we now have NavigationStack. This works fine when I replace NavigationView with NavigationStack - however my app needs to support iOS14+ so wondering what I can use for earlier versions...

Related

SwiftUI - OnExitCommand inside TabView

I have lately been trying to make a tvOS app, but have run into the following rather annoying problem. I can't use navigation inside a TabView and still have the menu button on the remove take me back to the previous state.
struct TestView: View {
#State var selection : Int = 0
var body: some View {
TabView(selection: self.$selection) {
ExpView()
.tabItem {
HStack {
Image(systemName: "magnifyingglass")
Text("Explore")
}
}
.tag(0)
}
}
}
struct ExpView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}
My question is: Is there any way to enable the menu button to go back to the previous view in the hierachy without dismissing the app completely?
You don't need to call dismiss on Menu it is called automatically for NavigationLink (so calling one more dismiss quits to main menu)
Here are fixed views. Tested with Xcode 11.4
struct ExploreView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}
So I found a workaround for the issue.
If you place the navigationView outside the TabView and then use the following code it works:
struct TestView: View {
#State var selection : Int = 0
#State var hideNavigationBar : Bool
var body: some View {
NavigationView {
TabView(selection: self.$selection) {
ExpView(hideNavigationBar: self.$hideNavigationBar)
.tabItem {
HStack {
Image(systemName: "magnifyingglass")
Text("Explore")
}
}
.tag(0)
}
}
}
}
struct ExpView: View {
#Binding var hideNavigationBar : Bool
var body: some View {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}.navigationBarTitle("")
.navigationBarHidden(self.hideNavigationBar)
.onAppear {
self.hideNavigationBar = true
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}

SwiftUI hiding a navigation bar only when looking at ContentView

I have a Content file and am hiding the navigation bar because it takes up space and pushes elements down. One of the buttons in the ContentView redirects (using a navigation link) to another view. In this other view, the navigationBar is still hidden....for simplicity sake, I'll cut out some of the code from ContentView:
//this is the view that looks "fine" (i.e. the navigation bar takes up no space)
struct ContentView: View {
#State private var isPresentedSettings = false
var body: some View {
NavigationView {
ZStack {
VStack {
SettingsButton(isPresentedSettings: $isPresentedSettings)
}
}.navigationBarTitle("").navigationBarHidden(true)
}
}
}
//this is the button that pulls up the settings page view
struct SettingsButton: View {
#Binding var isPresentedSettings: Bool
var body: some View {
NavigationLink (destination: SettingsPageView(isPresentedSettings:
self.$isPresentedSettings)) {
Button(action: { self.isPresentedSettings.toggle() }, label: { Text("Button") })
}
}
}
//This is the view that should have a navigationbar but it doesn't
struct SettingsPageView: View {
#Binding var isPresentedSettings: Bool
var body: some View {
NavigationView {
VStack {
Text("This is a view")
}.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
Also...there may have been typos because I just copied the code over from another computer. Sorry and thank you in advance!
Firstly, you don't need to have this isPresentedSettings variable for presenting a NavigationLink.
NavigationLink(destination: SettingsPageView()) {
Text("Button")
}
And there should be only one NavigationView in your view hierarchy.
This is how your final code can look like:
struct ContentView: View {
#State private var navBarHidden = true
var body: some View {
NavigationView {
ZStack {
VStack {
SettingsButton(navBarHidden: $navBarHidden)
}
}
.navigationBarHidden(navBarHidden)
}
}
}
struct SettingsButton: View {
#Binding var navBarHidden: Bool
var body: some View {
NavigationLink(destination: SettingsPageView(navBarHidden: $navBarHidden)) {
Text("Show View")
}
}
}
struct SettingsPageView: View {
#Binding var navBarHidden: Bool
var body: some View {
VStack {
Text("This is a view")
}
.navigationBarTitle("Settings", displayMode: .inline)
.onAppear {
self.navBarHidden = false
}
.onDisappear {
self.navBarHidden = true
}
}
}

SwiftUI TabView with List not refreshing after objected deleted from / added to Core Data

Description:
When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.
Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)
struct DetailView: View {
#ObservedObject var event: Event
var body: some View {
Text("\(event.timestamp!, formatter: dateFormatter)")
.navigationBarTitle(Text("Detail"))
}
}
Steps to reproduce:
Create a new Master Detail App project with SwiftUI and Core Data.
In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.
struct ContentView: View {
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
TabView {
NavigationView {
MasterView()
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { Event.create(in: self.viewContext) }
}
) {
Image(systemName: "plus")
}
)
Text("Detail view content goes here")
.navigationBarTitle(Text("Detail"))
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.tabItem { Text("Main") }
Text("Other Tab")
.tabItem { Text("Other Tab") }
}
}
}
Add a few items. Interact with those items in any way.
Change tabs.
Change back to Main Tab.
Attempt to delete an item.
I found a pure SwiftUI working solution:
/// This View that init the content view when selection match tag.
struct SyncView<Content: View>: View {
#Binding var selection: Int
var tag: Int
var content: () -> Content
#ViewBuilder
var body: some View {
if selection == tag {
content()
} else {
Spacer()
}
}
}
You can use it then in this way:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
SyncView(selection: $selection, tag: 0) {
ViewThatNeedsRefresh()
}
.tabItem { Text("First") }
.tag(0)
Text("Second View")
.font(.title)
.tabItem { Text("Second") }
.tag(1)
}
}
}
You can use the SyncView for each view that needs a refresh.

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 to navigate to a new view from navigationBar button click in SwiftUI

Learning to SwiftUI. Trying to navigate to a new view from navigation bar buttton clicked.
The sample code below:
var body: some View {
NavigationView {
List(0...< 5) { item in
NavigationLink(destination: EventDetails()){
EventView()
}
}
.navigationBarTitle("Events")
.navigationBarItems(trailing:
NavigationLink(destination: CreateEvent()){
Text("Create Event")
}
)
}
}
Three steps got this working for me : first add an #State Bool to track the showing of the new view :
#State var showNewView = false
Add the navigationBarItem, with an action that sets the above property :
.navigationBarItems(trailing:
Button(action: {
self.showNewView = true
}) {
Text("Go To Destination")
}
)
Finally add a navigation link somewhere in your view code (this relies on also having a NavigationView somewhere in the view stack)
NavigationLink(
destination: MyDestinationView(),
isActive: $showNewView
) {
EmptyView()
}.isDetailLink(false)
Put the NavigationLink into the label of a button.
.navigationBarItems(
trailing: Button(action: {}, label: {
NavigationLink(destination: NewView()) {
Text("")
}
}))
This works for me:
.navigationBarItems(trailing: HStack { AddButton(destination: EntityAddView()) ; EditButton() } )
Where:
struct AddButton<Destination : View>: View {
var destination: Destination
var body: some View {
NavigationLink(destination: self.destination) { Image(systemName: "plus") }
}
}
It is an iOS13 bug at the moment: https://forums.developer.apple.com/thread/124757
The "sort-of" workaround can be found here: https://stackoverflow.com/a/57837007/4514671
Here is my solution:
MasterView -
import SwiftUI
struct MasterView: View {
#State private var navigationSelectionTag: Int? = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DestinationView(), tag: 1, selection: self.$navigationSelectionTag) {
EmptyView()
}
Spacer()
}
.navigationBarTitle("Master")
.navigationBarItems(trailing: Button(action: {
self.navigationSelectionTag = 1
}, label: {
Image(systemName: "person.fill")
}))
}
}
}
struct MasterView_Previews: PreviewProvider {
static var previews: some View {
MasterView()
}
}
And the DetailsView -
import SwiftUI
struct DetailsView: View {
var body: some View {
Text("Hello, Details!")
}
}
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView()
}
}