I am trying to do programmatic navigation in NavigationView, but for some reason I am unable to switch between the views. When switching from the parent view everything works fine - but as soon as I am trying to switch while being in one of the child views I get this strange behaviour (screen is switching back and forth). I tried disabling animations, but this did not help. Strangely enough, if I remove a list together with .navigationViewStyle(StackNavigationViewStyle()) everything starts to work - but I need a list.
This seems to be somewhat similar to Deep programmatic SwiftUI NavigationView navigation but I do not have deep nesting and it still does not work.
I am using iOS 14.
struct TestView: View {
#State private var selection: String? = nil
var body: some View {
VStack {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("View A"), tag: "A", selection: self.$selection) { Text("A") }
NavigationLink(destination: Text("View B"), tag: "B", selection: self.$selection) { Text("B") }
}
}
.navigationTitle("Navigation")
}
.navigationViewStyle(StackNavigationViewStyle())
Button("Tap to show A") {
selection = "A"
}.padding()
Button("Tap to show B") {
selection = "B"
}.padding()
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Here is the behaviour i get:
Navigation View/Link is meant to operate from parent to child directly, if you break that order then you should not use navigate via NavLink.
What you need to do is use a fullScreenCover which I think solves your problem nicely. Copy and paste the code to see what I mean.
import SwiftUI
struct TestNavView: View {
#State private var selection: String? = nil
#State private var isShowing = false
#Environment(\.presentationMode) var pMode
var body: some View {
VStack {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("View A"), tag: "A", selection: self.$selection) { Text("A") }
NavigationLink(destination: Text("View B"), tag: "B", selection: self.$selection) { Text("B") }
}.fullScreenCover(isPresented: $isShowing, content: {
CView()
})
}
.navigationTitle("Navigation")
}
.navigationViewStyle(StackNavigationViewStyle())
Button("Tap to show A") {
selection = "A"
}.padding()
Button("Tap to show B") {
isShowing = true
selection = "B"
}.padding()
Button("Tap to show C") {
isShowing = true
}.padding()
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestNavView()
}
}
struct CView: View {
#Environment(\.presentationMode) var pMode
var body: some View {
VStack {
Button("Back") {self.pMode.wrappedValue.dismiss() }
Spacer()
Text("C")
Spacer()
}
}
}
If you are only wanting the presented view to take up half the screen, I would recommend using a ZStack to present the view over top of the main window.
You can add your own custom back button to the top left corner (or elsewhere).
This would allow both views to presented and switched between easily.
You can also add a withAnimation() to have the overlayed views to present nicely.
Related
I have a weird issue, where I navigate to EditView and edit a state variable from ContentView. After that I dismiss EditView with presentation.wrappedValue.dismiss(). The problem is, as soon as the view is dismissed, it reappears again.
I'm using XCode 12.4 and my iOS deployment target is set to 14.4
Observations:
EditView doesn't reappear if the value isn't edited
removing the changed value Text("Value: \(title)") >> Text("Value") from ContentView tree resolves the issue, but that obviously isn't a solution.
moving the NavigationLink from .toolbar e.g.
VStack {
Text("Value: \(title)")
NavigationLink(destination: EditView(title: $title)){
Text("Edit")
}
}
also resolves the issue, but that seems like a hack. Besides, I'd like to keep using .toolbar because I like the .navigationTitle animation and I can't have a button in the upper-right corner of the screen if I have a navigation title without the toolbar.
Here's the full code:
import SwiftUI
struct ContentView: View {
#State var title: String = "Title"
#State var isActive: Bool = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: EditView(title: $title, isActive: $isActive), isActive: $isActive){
Text("Edit")
}
}
}
}
}
}
struct EditView: View {
#Environment(\.presentationMode) var presentation
#Binding var title: String
#Binding var isActive: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
isActive = false
}){
Text("Done")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As far as I can tell this is a .toolbar bug, and if it turns out that way I'll report it to Apple, but in the meantime, does anyone have a better solution and/or explanation for this?
Cheers!
EDIT:
I updated the code with isActive value for the NavigationLink. It doesn't work when written like that, but uncommenting the commented out line makes it work. But that's quite hacky.
You are mixing up 2 things NavigationLink will push the view on stack, just like NavigationController in swift. That’s why you can see back button after navigating to second view. When you hit back button, it will pop top most view out of stack. presentationMode is not needed , dismissing presented view will not pop it of the stack.
To present a view and dismiss it you can check below code.
import SwiftUI
struct ContentViewsss: View {
#State var title: String = "Title"
#State var isPresented = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isPresented.toggle()
}){
Text("Edit")
}
}
}
}.sheet(isPresented: $isPresented, content: {
EditView(title: $title, state: $isPresented)
})
}
}
struct EditView: View {
#Binding var title: String
#Binding var state: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
state.toggle()
}){
Text("Done")
}
}
}
If you want NavigationLink functionality, you can just remove presentationMode code from second view, and keep ContentView as it is -:
struct EditView: View {
//#Environment(\.presentationMode) var presentation
#Binding var title: String
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
}){
Text("Done")
}
}
}
I recognized that my iOS app does have a double column navigation view style resp. split view for larger iPhones, like the iPhone 11 Pro Max, compared to a single column navigation.
I tried to get rid of this unwanted split view according to SwiftUI: unwanted split view on iPad by applying the .navigationViewStyle(StackNavigationViewStyle()) modifier to the NavigationView.
However, that introduces a new issue, where SwiftUI does not update the NavigationLink selection state after returning from a detail view. After coming back, the link is still shown to be active. After removing the .navigationViewStyle(StackNavigationViewStyle()) again, the selection state is updated correctly, so I think I am missing something.
I created a minimal reproducible example project. Please see the code below.
This image demonstrates the issue of a non-updating NavigationLink selection state after returning from a detail view when using the .navigationViewStyle(StackNavigationViewStyle()) modifier.
Minimal reproducible example project:
import SwiftUI
#main
struct TestSwiftUIApp: App {
var body: some Scene {
WindowGroup {
View1()
}
}
}
struct View1: View {
var body: some View {
NavigationView {
List {
NavigationLink(
destination: View2(),
label: {
Text("View2")
}
).isDetailLink(false)
NavigationLink(
destination: View2(),
label: {
Text("View2")
}
).isDetailLink(false)
}.listStyle(InsetGroupedListStyle())
.navigationTitle("View1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct View2: View {
#State var presentView3: Bool = false
var body: some View {
List {
Text("Foo")
NavigationLink("View3",
destination: View3(presentView3: $presentView3),
isActive: $presentView3
).isDetailLink(false)
Text("Bar")
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("View 2")
}
}
struct View3: View {
#Binding var presentView3: Bool
#State
var isAddViewPresented: Bool = false
var body: some View {
List {
Button(action: {presentView3 = false}, label: {
Text("Dismiss")
})
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("View3")
.toolbar {
ToolbarItem {
Button(action: {isAddViewPresented.toggle()}, label: {
Label("Add", systemImage: "plus.circle.fill")
})
}
}
.sheet(isPresented: $isAddViewPresented, content: {
Text("DestinationDummyView")
})
}
}
I am calling API when tab item is appeared if there is any changes. Why onAppear called after called onDisappear?
Here is the simple example :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
Text("Home")
.navigationTitle("Home")
.onAppear {
print("Home appeared")
}
.onDisappear {
print("Home disappeared")
}
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView {
Text("Account")
.navigationTitle("Account")
.onAppear {
print("Account appeared")
}
.onDisappear {
print("Account disappeared")
}
}
.tabItem {
Image(systemName: "gear")
Text("Account")
}.tag(1)
}
}
}
Just run above code and we will see onAppear after onDisappear.
Home appeared
---After switch tab to Account---
Home disappeared
Account appeared
Home appeared
Is there any solution to avoid this?
It's very annoying bug, imagine this scenario:
Home view onAppear method contains a timer which is fetching data repeatedly.
Timer is triggered invisibly by switching to the Account view.
Workaround:
Create a standalone view for every embedded NavigationView content
Pass the current selection value on to standalone view as #Binding parameter
E.g.:
struct ContentView: View {
#State var selected: MenuItem = .HOME
var body: some View {
return TabView(selection: $selected) {
HomeView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(MenuItem.HOME)
AccountView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "gear")
Text("Account")
}
}
.tag(MenuItem.ACCOUNT)
}
}
}
enum MenuItem: Int, Codable {
case HOME
case ACCOUNT
}
HomeView:
struct HomeView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Home")
.onAppear(perform: {
if MenuItem.HOME == selectedMenuItem {
print("-> HomeView")
}
})
}
}
AccountView:
struct AccountView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Account")
.onAppear(perform: {
if MenuItem.ACCOUNT == selectedMenuItem {
print("-> AccountView")
}
})
}
}
To whom it may help.
Because this behaviour I only could reproduce on iOS 14+, I end up using https://github.com/NicholasBellucci/StatefulTabView (which properly only get called when showed; but don't know if it's a bug or not, but it works with version 0.1.8) and TabView on iOS 13+.
I'm not sure why you are seeing that behaviour in your App. But I can explain why I was seeing it in my App.
I had a very similar setup to you and was seeing the same behaviour running an iOS13 App on iOS14 beta. In my Home screen I had a custom Tab Bar that would animate in and out when a detail screen was displayed. The code for triggering the hiding of the Tab Bar was done in the .onAppear of the Detail screen. This was triggering the Home screen to be redrawn and the .onAppear to be called. I removed the animation and found a much better set up due to this bug and the Home screen .onAppear stopped being called.
So if you have something in your Account Screen .onAppear that has a visual effect on the Home Screen then try commenting it out and seeing if it fixes the issue.
Good Luck.
I have been trying to understand this behavior for a number of days now. If you are working with a TabView, all of your onAppears() / onDisapear() will fire immediately on app init and never again. Which actually makes since I guess?
This was my solution to fix this:
import SwiftUI
enum TabItems {
case one, two
}
struct ContentView: View {
#State private var selection: TabItems = .one
var body: some View {
TabView(selection: $selection) {
ViewOne(isSelected: $selection)
.tabBarItem(tab: .one, selection: $selection)
ViewTwo(isSelected: $selection)
.tabBarItem(tab: .two, selection: $selection)
}
}
}
struct ViewOne: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View One")
.onChange(of: isSelected) { _ in
if isSelected == .one {
// Do something
}
}
}
}
struct ViewTwo: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View Two")
.onChange(of: isSelected) { _ in
if isSelected == .two {
// Do something
}
}
}
}
View Modifier for custom TabView
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifer: ViewModifier {
let tab: TabBarItem
#Binding var selection: TabBarItem
func body(content: Content) -> some View {
content
.opacity(selection == tab ? 1.0 : 0.0)
.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
modifier(TabBarItemViewModifer(tab: tab, selection: selection))
}
}
I am implementing List in Presented view (AddItemView). I want background color same as List in any view.
struct HomeView: View {
#State private var showAddItemView: Bool = false
var body: some View {
NavigationView {
List(0..<9, id: \.self) { i in
Text("Row \(i)")
}
.navigationTitle("Home")
.navigationBarItems(trailing:
Button("Add") {
showAddItemView.toggle()
})
.sheet(isPresented: $showAddItemView) {
AddItemView()
}
}
}
}
struct AddItemView: View {
init(){
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
NavigationView {
List(0..<9, id: \.self) { i in
Text("Row \(i)")
}.background(Color(UIColor.systemGroupedBackground))
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Add Item View", displayMode: .inline)
}
}
}
Above code is creating simple List with InsetGroupedListStyle. But background colour is different while Presenting view (AddItemView in my case).
I have already tried https://stackoverflow.com/a/58427518/7084910
How to set background color of List in presented view as in any normal list. Red/Yellow/Green can set to List, "BUT" I want same as normal list in HomeView that will work in light & dark mode.
Use this:
var body: some View {
NavigationView {
List(0..<9, id: \.self) { i in
Text("Row \(i)")
}
.colorMultiply(Color.red)
}
}
They think it is better visual representation for .sheet (probably to make it more determinable)...
SwiftUI 2.0
The .fullScreenCover gives what you want. Alternate is to present AddItemView manually using some transition.
.navigationBarItems(trailing:
Button("Add") {
showAddItemView.toggle()
})
.fullScreenCover(isPresented: $showAddItemView) {
AddItemView()
}
I am aware of how to dismiss a modal from a child view using #Environment (\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss() but this is a different issue.
When you present a multi-page NavigationView in a modal window, and have navigated through a couple of pages, the reference to presentationMode changes to be the NavigationView, so using self.presentationMode.wrappedValue.dismiss() simply pops the last NavigationView rather than dismissing the containing modal.
Is it possible - and if so how - to dismiss the containing modal from a page in a NavigationView tree?
Here's a simple example showing the problem. If you create an Xcode Single View app project using SwiftUI and replace the default ContentView code with this, it should work with no further changes.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
PageOneContent()
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// How to dismiss parent modal here instead
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Here is possible approach based on usage own explicitly created environment key (actually I have feeling that it is not correct to use presentationMode for this use-case.. anyway).
Proposed approach is generic and works from any view in modal view hierarchy. Tested & works with Xcode 11.2 / iOS 13.2.
// define env key to store our modal mode values
struct ModalModeKey: EnvironmentKey {
static let defaultValue = Binding<Bool>.constant(false) // < required
}
// define modalMode value
extension EnvironmentValues {
var modalMode: Binding<Bool> {
get {
return self[ModalModeKey.self]
}
set {
self[ModalModeKey.self] = newValue
}
}
}
struct ParentModalTest: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
}) {
PageOneContent()
.environment(\.modalMode, self.$showModal) // < bind modalMode
}
}
}
struct PageOneContent: View {
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
trailing: NavigationLink(destination: PageTwoContent()) {
Text("Next")
})
}
}
}
struct PageTwoContent: View {
#Environment (\.modalMode) var modalMode // << extract modalMode
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
self.modalMode.wrappedValue = false // << close modal
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
Another Approach would be to simply use a notification for this case and just reset the triggering flag for your modal.
It is not the most beautiful solution for me but it is the solution I am most likely to still understand in a few months.
import SwiftUI
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView()
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
var body: some View {
Text("Pushed View").onTapGesture {
NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "PushedViewNotifciation")))
}
}
}
If you don't want to loosely couple the views through a notification you could also just use a binding for this like so:
struct ContentView: View {
#State var showModalNav: Bool = false
var body: some View {
Text("Present Modal")
.padding()
.onTapGesture {
showModalNav.toggle()
}.sheet(isPresented: $showModalNav, content: {
ModalNavView(parentShowModal: $showModalNav)
}).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
showModalNav = false
}
}
}
struct ModalNavView: View {
#Binding var parentShowModal: Bool
var body: some View {
NavigationView {
NavigationLink(
destination: PushedView(parentShowModal: $parentShowModal),
label: {
Text("Show Another View")
}
)
}
}
}
struct PushedView: View {
#Binding var parentShowModal: Bool
var body: some View {
Text("Pushed View").onTapGesture {
parentShowModal = false
}
}
}
If it's only two levels, and especially if you can dismiss the sheet at multiple levels, you could include showModal as a binding variable in your navigation views below, and then toggling it anywhere would dismiss the entire sheet.
I would assume you could do something similar with showModal as an EnvironmentObject as Wei mentioned above - which might be better if there are more than two levels and you only want to dismiss the sheet at the most specific level.
I can't remember if there's some reason to move away from doing this as a binding variable, but it seems to be working for me.
import SwiftUI
struct ContentView: View {
#State var showModal: Bool = false
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Launch Modal")
}
.sheet(isPresented: self.$showModal, onDismiss: {
self.showModal = false
}) {
// Bind showModal to the corresponding property in PageOneContent
PageOneContent(showModal: $showModal)
}
}
}
Then you add showModal as a binding variable in PageOneContent, and it is bound to the state variable in ContentView.
struct PageOneContent: View {
// add a binding showModal var in here
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("I am Page One")
}
.navigationBarTitle("Page One")
.navigationBarItems(
// bind showModal again to PageTwoContent
trailing: NavigationLink(destination: PageTwoContent(showModal: $showModal)) {
Text("Next")
})
}
}
}
Finally, in PageTwoContent, you can add showModal here (and in the NavigationLink in PageOneContent, you have bound PageTwoContent's showModal to PageOneContent). Then in your button, all you have to do is toggle it, and it will dismiss the sheet.
struct PageTwoContent: View {
// Add showModal as a binding var here too.
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack {
Text("This should dismiss the modal. But it just pops the NavigationView")
.padding()
Button(action: {
// This will set the showModal var back to false in all three views, and will dismiss the current sheet.
self.showModal.toggle()
}) {
Text("Finish")
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
}
.navigationBarTitle("Page Two")
}
}
}
I found out you can actually make showModal into an EnvironmentObject, then simplify toggle the showModal to false on PageTwoContent to dismiss both PageOneContent and PageTwoContent.