SwiftUI: Go to a new view from inside a modal - ios

I would like to go to a new view from inside my modal.
Currently I have my default view. I then present my modal. In that modal I can close it but I also have a another button that I would like to take me to another view. (NewView)
IS this possible?
Default view:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
var body: some View {
Button("Show Modal") {
self.showModal.toggle()
}.sheet(isPresented: $showModal) {
ModalView(showModal: self.$showModal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Modal View:
struct ModalView: View {
#Binding var showModal: Bool
var body: some View {
VStack (spacing: 30){
Text("My Modal View")
.font(.title)
.padding()
Button("Close modal") {
self.showModal.toggle()
}
Button("Close modal and go to a new view") {
}
}
}
}
NewView:
struct NewView: View {
var body: some View {
Text("My New View")
}
}

I understood your question in 2 different ways, and luckily I have both solutions.
If you want the following flow: ViewA -> Opens ViewB As modal -> opens ViewC as NavigatedView in ViewB
Then all you need to change is your ModalView to this
struct ModalView: View {
#Binding var showModal: Bool
var body: some View {
NavigationView {
VStack (spacing: 30){
NavigationLink(destination: NewView()) {
Text("Take me to NewView")
}
Text("My Modal View")
.font(.title)
.padding()
Button("Close modal") {
self.showModal.toggle()
}
Button("Close modal and go to a new view") {
}
}
}
}
}
IF however what you mean is ViewA -> opens ViewsB as Moda -> ViewB then tells ViewA to navigate to ViewC
then please refer to this link, someone asked this question earlier and I provided the solution
SwiftUI transition from modal sheet to regular view with Navigation Link

Related

How to dismiss a SwiftUI NavigationView from a parent view

Usually, to implement a "custom dismiss button" within a SwiftUI NavigationView, I would use #Environment(\.dismiss) private var dismiss, and then call dismiss() to dismiss the view.
However, suppose I want to dismiss a NavigationLink from a parent of the NavigationView?
struct SwiftUIView: View {
var body: some View {
HStack {
Button(action: {
// dismiss view
}) {
Text("Dismiss View")
} // assume this button is always visible
NavigationView {
Text("This is the NavigationView")
NavigationLink {
Text("This is the view I want to exit")
} label: {
Text("Go to view I want to exit")
}
}
}
}
}
How would I go about dismissing the view displaying This is the view I want to exit, by clicking the Dismiss View button in the parent view?
Here is the solution:
struct SwiftUIView: View {
#State var isPresent = true
#State var isDismissed: Bool = false
var body: some View {
HStack {
Button(action: {
isDismissed = true
}) {
Text("Dismiss View")
} // assume this button is always visible
NavigationView {
VStack {
Text("This is the NavigationView")
NavigationLink {
ViewToDismiss(dismissView: $isDismissed)
} label: {
Text("Go to view I want to exit")
}
}
}
}
}
}
struct ViewToDismiss: View {
#Environment(\.dismiss) private var dismiss
#Binding var dismissView: Bool
var body: some View {
Text("This is the view I want to exit")
.onChange(of: dismissView,
perform:
( { newValue in
dismiss()
}))
}
}

SwiftUI - Attempt to present SwiftUI.PlatformAlertController... whose view is not in the window hierarchy

While this question has been asked previously, none of the threads deal with the latest SwiftUI and iOS15+.
I am seeing this error in console when navigating to a second level in a custom tab view, where the NavigationView is instantiated outside of the custom tab view.
The full error is:
[Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x7f8962874800> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f896080dac0>) whose view is not in the window hierarchy
The hierarchy is as follows:
Login Page (NavigationView instantiated here) -> Tap NavigationLink
from this view
Custom Tab View is displayed -> Tap any tab and see
the correct view
Tap a NavigationLink from within one of these views
and the alert works perfectly
Tap a NavigationLink from within that view and the error is shown and no alerts work
I know it's a lot of code, but I have been asked to provide code that will compile, and since this is quite a few different files to make this happen, I stripped everything down to the bare minimum so the issue could be recreated.
Here's the code:
App:
import SwiftUI
#main
struct TestApp: App {
#StateObject var viewRouter: ViewRouter
init() {
let viewRouter = ViewRouter()
_viewRouter = StateObject(wrappedValue: viewRouter)
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewRouter)
}
}
}
ViewRouter:
import Foundation
class ViewRouter: ObservableObject {
#Published var accessView: DisplayView = .home
}
enum DisplayView {
case settings
case home
}
ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: HomeView()
) {
Text("Go To Tab View")
}
.isDetailLink(false)
}
}
}
HomeView:
import SwiftUI
struct HomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
switch viewRouter.accessView {
case .home:
UserHome()
.navigationBarHidden(true)
case .settings:
UserSettings()
.navigationBarHidden(true)
}
}
}
struct HomeNavView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 30) {
HStack(alignment: .bottom) {
Image(systemName: "House")
.onTapGesture {
viewRouter.accessView = .home
}
.frame(maxWidth: .infinity)
Image(systemName: "Person")
.onTapGesture {
viewRouter.accessView = .settings
}
.frame(maxWidth: .infinity)
}
}
}
.ignoresSafeArea()
.zIndex(1)
}
}
UserHome:
import SwiftUI
struct UserHome: View {
var body: some View {
Text("User Home View")
HomeNavView()
}
}
UserSettings:
import SwiftUI
struct UserSettings: View {
var body: some View {
NavigationLink(
destination: SectionView()
) {
Text("Go To Section View")
}
}
}
SectionView (if you try to display an alert here, it works just fine):
import SwiftUI
struct SectionView: View {
var body: some View {
NavigationLink(
destination: AlertView()
) {
Text("Go To Alert View")
}
}
}
AlertView (This is where you see the error in console and the alert does not work):
import SwiftUI
struct AlertView: View {
#State private var alertTest: Bool = false
var body: some View {
Button("Show Alert") {
alertTest = true
}
.alert(
"Alert",
isPresented: $alertTest,
actions: {
Button("OK", role: .cancel) {
}
}, message: {
Text("The alert is working!")
})
}
}

SwiftUI NavigationView with List programmatic navigation does not work

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.

onDisappear not called when a modal View is dismissed

I rely on the SwiftUI's .onDisappear to perform some logic but it is not being called when the user dismisses a modally presented view with the swipe gesture. To reproduce
Present a view modally a "ChildView 1"
On this view, push a "ChildView 2" as a navigation child
Swipe down to dismiss the modal view.
The .onDisappear of "ChildView 2" is not called.
Sample code to reproduce
import SwiftUI
struct ContentView: View {
#State var isShowingModal
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
struct ChildView: View {
let title: Int
var body: some View {
NavigationLink(destination: ChildView(title: title + 1)) {
Text("Show Child")
}
.navigationBarTitle("View \(title)")
.onAppear {
print("onAppear ChildView \(self.title)")
}
.onDisappear {
print("onDisappear ChildView \(self.title)")
}
}
}
The output is:
onAppear ChildView 1
onAppear ChildView 2
onDisappear ChildView 1
If you're looking for logic to occur when the actual modal is dismissed, you're going to want to call that here, where I print out Modal Dismissed:
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
.onDisappear {
print("Modal Dismissed")
}
}
}
}
struct ContentView: View {
#State var isShowingModal = false
var body: some View {
NavigationView {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal")
}
}
.sheet(isPresented: $isShowingModal) {
NavigationView {
ChildView(title: 1)
}
}
}
}
in this code, you have NavigationView, and when presenting sheet, you push there another NavigationView. This is the source of trouble
You don't need any NavigationView to present modals. If you like to present another modal from modal, you can use
import SwiftUI
struct ContentView: View {
var body: some View {
ChildView(title: 1)
}
}
struct ChildView: View {
#State var isShowingModal = false
let title: Int
var body: some View {
Button(action: {
self.isShowingModal.toggle()
}) {
Text("Show Modal \(title)").font(.largeTitle)
}
.sheet(isPresented: $isShowingModal) {
ChildView(title: self.title + 1)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
UPDATE
from Apple
Human Interface Guidelines
Modality is a design technique that presents content in a temporary mode that’s separate from the user's previous current context and requires an explicit action to exit

Dismiss a parent modal in SwiftUI from a NavigationView

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.

Resources