Can't dismiss modal in SwiftUI - ios

I have been having issues dismissing some modal views in SwiftUI and made the following example to illustrate the problem.
Below we have 4 views. The idea is that the App file will have a switch and decide which view to display based on that Switch's viewForDisplay property.
Initially we display the FirstView which modally presents the SecondView which then modally presents the ThirdView. When the ThirdView sets the viewForDisplay to .fourthView I would expect all the views in the FirstView/SecondView/ThirdView stack to go away and just see the FourthView. However it is showing the SecondView.
enum ViewForDisplay {
case firstView
case fourthView
}
class ViewModel: ObservableObject {
#Published var viewForDisplay: ViewForDisplay = .firstView
}
#main
struct ModalDismissApp: App {
#ObservedObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
switch viewModel.viewForDisplay {
case .firstView:
FirstView(viewModel: viewModel)
case .fourthView:
FourthView()
}
}
}
}
struct FirstView: View {
#State var isPresented: Bool = false
#ObservedObject var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Text("First View")
Button(action: {
isPresented = true
}, label: {
Text("Present Second View Modally")
})
}
.fullScreenCover(isPresented: $isPresented, content: {
SecondView(viewModel: viewModel)
})
}
}
struct SecondView: View {
#State var isPresented: Bool = false
#ObservedObject var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Text("Second View")
Button(action: {
isPresented = true
}, label: {
Text("Present Third View Modally")
})
}
.fullScreenCover(isPresented: $isPresented, content: {
ThirdView(viewModel: viewModel)
})
}
}
struct ThirdView: View {
#ObservedObject var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Text("Third View")
Button(action: {
viewModel.viewForDisplay = .fourthView
}, label: {
Text("Dismiss Modals and go to Fourth View")
})
}
}
}
struct FourthView: View {
var body: some View {
Text("Fourth View")
}
}
This only happens when two levels of modal are applied. For example, if I were to set the viewForDisplay to .fourthView from the SecondView everything works fine. But for some reason when I have more than one modal it doesn't work.
I can work around this by dismissing the ThirdView and then setting the .viewForDisplay property but that gives me an undesirable animation. I just want to go directly to my FourthView and not sure why with multiple modals this is an issue.

You first need to dismiss all the presented controllers and then switch to the fourth view.
Here is the easy possible solution.
In the 3rd view, before switching to the fourth view, just dismiss all views.
struct ThirdView: View {
#ObservedObject var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Text("Third View")
Button(action: {
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true, completion: {
}) //<-- Dismisss all view
viewModel.viewForDisplay = .fourthView
}, label: {
Text("Dismiss Modals and go to Fourth View")
})
}
}
}

Related

In SwiftUI Is it possible for top level view to overlay a child's modal view

I am trying to overlay the modal view of a child in the parent's ZStack doing the following:
class ViewModel: ObservableObject {
#Published var showGreen = false
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var showModal = false
var body: some View {
ZStack {
Color.red
.onTapGesture {
showModal = true
}
.fullScreenCover(isPresented: $showModal) {
ModalView()
.environmentObject(viewModel)
}
if viewModel.showGreen {
Color.green
}
}
}
}
struct ModalView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
ZStack {
Color.blue
}
.onTapGesture {
viewModel.showGreen = true
}
}
}
Using this setup, is there anyway in the parent (ContentView in this case) to show a view that not only covers it's children but also the modally presented view's of it's children?
I know there are ways to do this by overlaying on the children themselves but that is not what I am after.
The easiest way to cover it inside ModalView
struct ModalView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
ZStack {
Color.blue
if viewModel.showGreen {
Color.green
}
}
.onTapGesture {
viewModel.showGreen = true
}
}
}
Another hack is to present one more modal view
ModalView()
.fullScreenCover(isPresented: $viewModel.showGreen) {
Color.green
}
.environmentObject(viewModel)
If you really need to build your cover outside of ModalView, pass it as a parameter
class ViewModel: ObservableObject {
#Published var showGreen = false
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var showModal = false
var body: some View {
ZStack {
Color.red
.onTapGesture {
showModal = true
}
.fullScreenCover(isPresented: $showModal) {
ModalView(content: {
if viewModel.showGreen {
Color.green
}
})
.environmentObject(viewModel)
}
}
}
}
struct ModalView<Content: View>: View {
#EnvironmentObject var viewModel: ViewModel
#ViewBuilder var content: () -> Content
var body: some View {
ZStack {
Color.blue
content()
}
.onTapGesture {
viewModel.showGreen = true
}
}
}
UPDATE: One more way without modifying ModalView
ModalView()
.overlay(content: {
if viewModel.showGreen {
Color.green
}
})
.environmentObject(viewModel)

What is the best way to pass binding to each view in navigation stack in SwiftUI

I am transitioning from one view to another using .fullScreenCover. The presented view has a navigation stack and will consist of several views. I want each one of those views to be able to dismiss the entire stack using the binded isActive property used in the .fullScreenCover.
Do I need to bind this property to each view in the stack or is there a simpler way to do this. I thought of adding this property to the viewModel of the navigation stack but had no luck getting it there. Here is what I am trying to do:
struct FirstView: View {
#State var isActive = false
var body: some View {
Text("Press to present Navigation Stack")
.onTapGesture {
isActive = true
}
.fullScreenCover(isPresented: $isActive, content: {
SecondView(isActive: $isActive)
})
}
}
struct SecondView: View {
#StateObject var viewModel: ViewModel = ViewModel()
#Binding var isActive: Bool
#State var showThirdViewIsActive: Bool = false
init(isActive: Binding<Bool>) {
self._isActive = isActive
// somehow get the isActive to my ViewModel but no luck
}
var body: some View {
NavigationView {
Button(action: {
$showThirdViewIsActive = true
}, label: {
Text("Show Third View")
})
Button(action: {
viewModel.dismiss()
}, label: {
Text("Dismiss")
})
NavigationLink(
destination: ThirdView(viewModel: viewModel, isActive: $isActive),
isActive: $showThirdViewIsActive) {}
}
}
}
struct ThirdView: View {
#ObservableObject var viewModel: ViewModel
#Binding var isActive: Bool
init(viewModel: ViewModel, isActive: Binding<Bool>) {
self.viewModel = viewModel
self._isActive = isActive
// somehow get the isActive to my ViewModel but no luck
}
var body: some View {
Button(action: {
viewModel.dismiss()
}, label: {
Text("dismiss")
})
}
}
class ViewModel: ObservableObject {
#Binding var isActive: Bool
func dismiss() {
self.isActive = false
}
}
Of course I could just dismiss the SecondView by setting isActive to false but I am doing it in the ViewModel on purpose because it will not just be for this simple case. It may dismiss after an asynchronous call to access a server.
Also I have just shown two views but there will be NavigationView in the second view and NavigationLink to another view and so on. They will all share the same ViewModel. The FirstView does not share that ViewModel.
I don't want to have to pass the isActive to each of those views if I can avoid it. I want to either be able to pass the isActive somehow to the viewModel from the SecondView, or to have it be some kind of environment variable that can be accessed by any of the views.
Not sure what is the best practice in what I am guessing is this common scenario.
I don't know your project, I take for granted what you are doing.
If you want to dismiss by the model, the model has to be implemented inside the first view.
struct FirstView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
Text("Press to present Navigation Stack")
.onTapGesture {
viewModel.isActive = true
}
.fullScreenCover(isPresented: $viewModel.isActive, content: {
SecondView(viewModel: viewModel)
})
}
}
struct SecondView: View {
#StateObject var viewModel: ViewModel
#State var showThirdViewIsActive: Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showThirdViewIsActive = true
}, label: {
Text("Show Third View")
})
Button(action: {
viewModel.dismiss()
}, label: {
Text("Dismiss")
})
NavigationLink(
destination: ThirdView(viewModel: viewModel),
isActive: $showThirdViewIsActive) {}
}
}
}
}
struct ThirdView: View {
#StateObject var viewModel: ViewModel
var body: some View {
Button(action: {
viewModel.dismiss()
}, label: {
Text("dismiss")
})
}
}
class ViewModel: ObservableObject {
#Published var isActive: Bool = false
func dismiss() {
self.isActive = false
}
}
struct SwiftUIViewTest_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
UPDATE
using only #Binding
struct FirstView: View {
#State var isActive: Bool = false
var body: some View {
Text("Press to present Navigation Stack")
.onTapGesture {
isActive = true
}
.fullScreenCover(isPresented: $isActive, content: {
NavigationView {
SecondView(isActive: $isActive)
}
})
.onAppear() {
isActive = false
}
}
}
struct SecondView: View {
#Binding var isActive: Bool
#State var showThirdViewIsActive: Bool = false
var body: some View {
VStack {
Button(action: {
showThirdViewIsActive = true
}, label: {
Text("Show Third View")
})
Button(action: {
isActive = false
}, label: {
Text("Dismiss")
})
NavigationLink(
destination: ThirdView(isActive: $isActive),
isActive: $showThirdViewIsActive) {}
}
}
}
struct ThirdView: View {
#Binding var isActive: Bool
var body: some View {
Button(action: {
isActive = false
}, label: {
Text("dismiss")
})
}
}

SwiftUI transition from modal sheet to regular view with Navigation Link

I'm working with SwiftUI and I have a starting page. When a user presses a button on this page, a modal sheet pops up.
In side the modal sheet, I have some code like this:
NavigationLink(destination: NextView(), tag: 2, selection: $tag) {
EmptyView()
}
and my modal sheet view is wrapped inside of a Navigation View.
When the value of tag becomes 2, the view does indeed go to NextView(), but it's also presented as a modal sheet that the user can swipe down from, and I don't want this.
I'd like to transition from a modal sheet to a regular view.
Is this possible? I've tried hiding the navigation bar, etc. but it doesn't seem to make a difference.
Any help with this matter would be appreciated.
You can do this by creating an environmentObject and bind the navigationLink destination value to the environmentObject's value then change the value of the environmentObject in the modal view.
Here is a code explaining what I mean
import SwiftUI
class NavigationManager: ObservableObject{
#Published private(set) var dest: AnyView? = nil
#Published var isActive: Bool = false
func move(to: AnyView) {
self.dest = to
self.isActive = true
}
}
struct StackOverflow6: View {
#State var showModal: Bool = false
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: self.navigationManager.dest, isActive: self.$navigationManager.isActive) {
EmptyView()
}
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}
}
}
.sheet(isPresented: self.$showModal) {
secondView(isPresented: self.$showModal).environmentObject(self.navigationManager)
}
}
}
struct StackOverflow6_Previews: PreviewProvider {
static var previews: some View {
StackOverflow6().environmentObject(NavigationManager())
}
}
struct secondView: View {
#EnvironmentObject var navigationManager: NavigationManager
#Binding var isPresented: Bool
#State var dest: AnyView? = nil
var body: some View {
VStack {
Text("Modal view")
Button(action: {
self.isPresented = false
self.dest = AnyView(thirdView())
}) {
Text("Press me to navigate")
}
}
.onDisappear {
// This code can run any where but I placed it in `.onDisappear` so you can see the animation
if let dest = self.dest {
self.navigationManager.move(to: dest)
}
}
}
}
struct thirdView: View {
var body: some View {
Text("3rd")
.navigationBarTitle(Text("3rd View"))
}
}
Hope this helps, if you have any questions regarding this code, please let me know.

SwiftUI: detecting the NavigationView back button press

In SwiftUI I couldn't find a way to detect when the user taps on the default back button of the navigation view when I am inside DetailView1 in this code:
struct RootView: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}
struct DetailView1: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationLink(destination: DetailView2(), isActive: $showDetails) {
Text("show DetailView2")
}
.navigationBarTitle("DetailView1")
}
}
struct DetailView2: View {
var body: some View {
Text("")
.navigationBarTitle("DetailView2")
}
}
Using .onDisappear doesn't solve the problem as its closure is called when the view is popped off or a new view is pushed.
The quick solution is to create a custom back button because right now the framework have not this possibility.
struct DetailView : View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body : some View {
Text("Detail View")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}){
Image(systemName: "arrow.left")
})
}
}
As soon as you press the back button, the view sets isPresented to false, so you can use an observer on that value to trigger code when the back button is pressed. Assume this view is presented inside a navigation controller:
struct MyView: View {
#Environment(\.isPresented) var isPresented
var body: some View {
Rectangle().onChange(of: isPresented) { newValue in
if !newValue {
print("detail view is dismissed")
}
}
}
}
An even nicer (SwiftUI-ier?) way of observing the published showDetails property:
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
.onReceive(self.viewModel.$showDetails) { isShowing in
debugPrint(isShowing)
// Maybe do something here?
}
}
}
}
Following up on my comment, I would react to changes in the state of showDetails. Unfortunately didSet doesn't appear to trigger with #State variables. Instead, we can use an observable view model to hold the state, which does allow us to do intercept changes with didSet.
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false {
didSet {
debugPrint(showDetails)
// Maybe do something here?
}
}
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}

iOS SwiftUI: pop or dismiss view programmatically

I couldn't find any reference about any ways to make a pop or a dismiss programmatically of my presented view with SwiftUI.
Seems to me that the only way is to use the already integrated slide dow action for the modal(and what/how if I want to disable this feature?), and the back button for the navigation stack.
Does anyone know a solution?
Do you know if this is a bug or it will stays like this?
This example uses the new environment var documented in the Beta 5 Release Notes, which was using a value property. It was changed in a later beta to use a wrappedValue property. This example is now current for the GM version. This exact same concept works to dismiss Modal views presented with the .sheet modifier.
import SwiftUI
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"Here is Detail View. Tap to go back.",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}
struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView())
{ Text("I am Root. Tap for Detail View.") }
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}
SwiftUI Xcode Beta 5
First, declare the #Environment which has a dismiss method which you can use anywhere to dismiss the view.
import SwiftUI
struct GameView: View {
#Environment(\.presentationMode) var presentation
var body: some View {
Button("Done") {
self.presentation.wrappedValue.dismiss()
}
}
}
iOS 15+
Starting from iOS 15 we can use a new #Environment(\.dismiss):
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
(There's no more need to use presentationMode.wrappedValue.dismiss().)
Useful links:
DismissAction
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5. Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, an alternative navigation stack for SwiftUI. It offers several features described in the readme of the repo. For example, you can easily push and pop views programmatically. I'll show you how to do that with a simple example:
First of all embed your hierarchy in a NavigationStackVew:
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
NavigationStackView gives your hierarchy access to a useful environment object called NavigationStack. You can use it to, for instance, pop views programmatically as asked in the question above:
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
In this example I use the PushView to trigger the push navigation with a tap. Then, in the View2 I use the environment object to programmatically come back.
Here is the complete example:
import SwiftUI
import NavigationStack
struct RootView: View {
var body: some View {
NavigationStackView {
View1()
}
}
}
struct View1: View {
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 1")
Spacer()
PushView(destination: View2()) {
Text("PUSH TO VIEW 2")
}
}
}
}
}
struct View2: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
VStack {
Text("VIEW 2")
Spacer()
Button(action: {
self.navStack.pop()
}, label: {
Text("PROGRAMMATICALLY POP TO VIEW 1")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
the result is:
Alternatively, if you don't want to do it programatically from a button, you can emit from the view model whenever you need to pop.
Subscribe to a #Published that changes the value whenever the saving is done.
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
#Environment(\.presentationMode) var presentationMode
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
.textContentType(.name)
}
.onAppear {
self.viewModel.cancellable = self.viewModel
.$saved
.sink(receiveValue: { saved in
guard saved else { return }
self.presentationMode.wrappedValue.dismiss()
}
)
}
}
}
class ContentViewModel: ObservableObject {
#Published var saved = false // This can store any value.
#Published var name = ""
var cancellable: AnyCancellable? // You can use a cancellable set if you have multiple observers.
func onSave() {
// Do the save.
// Emit the new value.
saved = true
}
}
Please check Following Code it's so simple.
FirstView
struct StartUpVC: View {
#State var selection: Int? = nil
var body: some View {
NavigationView{
NavigationLink(destination: LoginView().hiddenNavigationBarStyle(), tag: 1, selection: $selection) {
Button(action: {
print("Signup tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Sign up")
Spacer()
}
}
}
}
}
SecondView
struct LoginView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView{
Button(action: {
print("Login tapped")
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Back")
.resizable()
.frame(width: 20, height: 20)
.padding(.leading, 20)
}
}
}
}
}
You can try using a custom view and a Transition.
Here's a custom modal.
struct ModalView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
if (!self.isShowing) {
self.content()
}
if (self.isShowing) {
self.content()
.disabled(true)
.blur(radius: 3)
VStack {
Text("Modal")
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.transition(.moveAndFade) // associated transition to the modal view
}
}
}
}
}
I reused the Transition.moveAndFade from the Animation Views and Transition tutorial.
It is defined like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale()
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
You can test it - in the simulator, not in the preview - like this:
struct ContentView: View {
#State var isShowingModal: Bool = false
func toggleModal() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
self.isShowingModal = false
}
}
}
}
var body: some View {
ModalView(isShowing: $isShowingModal) {
NavigationView {
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}.onAppear { self.toggleModal() }
}
}
}
Thanks to that transition, you will see the modal sliding in from the trailing edge, and the it will zoom and fade out when it is dismissed.
The core concept of SwiftUI is to watch over the data flow.
You have to use a #State variable and mutate the value of this variable to control popping and dismissal.
struct MyView: View {
#State
var showsUp = false
var body: some View {
Button(action: { self.showsUp.toggle() }) {
Text("Pop")
}
.presentation(
showsUp ? Modal(
Button(action: { self.showsUp.toggle() }) {
Text("Dismiss")
}
) : nil
)
}
}
I experienced a compiler issue trying to call value on the presentationMode binding. Changing the property to wrappedValue fixed the issue for me. I'm assuming value -> wrappedValue is a language update. I think this note would be more appropriate as a comment on Chuck H's answer but don't have enough rep points to comment, I also suggested this change as and edit but my edit was rejected as being more appropriate as a comment or answer.
This will also dismiss the view
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
window?.rootViewController?.dismiss(animated: true, completion: {
print("dismissed")
})

Resources