Get the view which was dismissed and from which the current view is display - ios

I have a NavigationView which is showing a different view via fullScreenCover when I press a Button. Now I need to know when my view from where I pressed the Button is getting visible again and what was the view before. In the view shown by the fullScreenCover im using #Environment(\.dismiss) var dismiss to dismiss it.
So here is my concrete use case:
I have my main screen with two Buttons A and B.
A is showing sub view 1 and B is showing sub view 2.
When I dismiss one of these sub screens I need to know if I was in A view 1 or 2 before.
Is this somehow possible?

You can use the .onDisappear on the view that is linked on the destination of the NavigationLink. See this example:
import SwiftUI
struct ContentView: View {
#State var selectedSubView: ViewSelection = ViewSelection.zero
var body: some View {
NavigationView {
VStack {
// Version 1
NavigationLink (
destination: SubView1()
.onDisappear {
self.selectedSubView = .one
print(self.selectedSubView) // debug
},
label: {
Text("To ViewOne").padding()
})
// Version 2
NavigationLink (
destination: SubView2()
.onDisappear {
self.selectedSubView = .two
print(self.selectedSubView) // debug
},
label: {
Text("To ViewTwo")
.padding()
})
}
}
}
}
struct SubView1: View {
var body: some View {
Text("View 1")
}
}
struct SubView2: View {
var body: some View {
Text("View 2")
}
}
enum ViewSelection {
case one
case two
case zero
}

You can use .fullScreenCover with an onDismiss closure.
(Here I used .sheet because it's easier to test, but it's the same)
struct SwiftUIView4: View {
#State private var destination: Destination?
#State private var showed: Destination?
#State private var dismissed: Destination?
var body: some View {
VStack {
HStack {
Button("A") {
destination = .firstView
showed = .firstView
}.padding()
Spacer()
Button("B") {
destination = .secondView
showed = .secondView
}.padding()
}
Text("Just dismissed : \(dismissed?.rawValue ?? "nothing")")
.sheet(item: $destination, onDismiss: {
dismissed = showed
}, content: { destination in
switch destination {
case .firstView: Text("A clicked")
case .secondView: Text("B clicked")
}
})
}
}
}
enum Destination: String, Identifiable {
case firstView, secondView
var id: Destination { self }
}
Or you could use a custom Binding for the item parameter of your .fullScreenCover :
struct SwiftUIView4: View {
#State private var showed: Destination?
#State private var dismissed: Destination?
var body: some View {
VStack {
HStack {
Button("A") {
showed = .firstView
}.padding()
Spacer()
Button("B") {
showed = .secondView
}.padding()
}
Text("Just dismissed : \(dismissed?.rawValue ?? "nothing")")
.sheet(item: .init(get: {showed}, set: {
dismissed = showed // HERE
showed = $0 // <<<<
})) { destination in
switch destination {
case .firstView: Text("A clicked")
case .secondView: Text("B clicked")
}
}
}
}
}

Related

Why is NavigationLink not working inside of a SwiftUI alert?

I have an alert that upon tapping I would like to return the user back to another view. The alert is showing, but why does it not navigate upon tapping?
VStack{
.alert("End of available content", isPresented: $model.alertIsPresented) {
NavigationLink(destination: SearchView()) {
Button("OK", role:.cancel) {}
}
}
}
Because NavigationLink needs to be inside of a hierarchy using NavigationView. An alert is a modal presented outside of that structure.
If you would like to programmatically navigate, you can use the isActive property of a NavigationLink within the NavigationView hierarchy.
struct ContentView : View {
#State private var alertIsPresented = false
#State private var navLinkActive = false
var body: some View {
NavigationView {
VStack{
Button("Present alert") {
alertIsPresented = true
}
.alert("End of available content", isPresented: $alertIsPresented) {
Button("Navigate") {
navLinkActive = true
}
}
NavigationLink(isActive: $navLinkActive, destination: { SearchView() }, label: {
EmptyView()
})
}
}
}
}
struct SearchView : View {
var body: some View {
Text("Search")
}
}

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.

Dismissing a View using a "static nav bar"

I've created a simple view acting as a navbar which contains a menu button and some text. I'm using this as a top-level element outside my NavigationView which allows me to have the view static across all child pages that come into view. The reason I'm trying not to use the default navbar, with navbar items, is to avoid the dismissal/creation that you get along with the fading animation when you switch views.
The problem I'm now facing is dismissing the child view's when I have navigated away from the parent view. I'm able to update the button from a menu icon to a back icon, but the action of the button is not triggered. Been looking online to see if anyone has done something similar but had no luck, I'm not sure what I'm trying to achieve is even possible or whether I am going about it the right way. Is there anyway to call self.presentationMode.wrappedValue.dismiss() from the child views even though the header is initialised in the root view? Any help is appreciated, here's what I have so far:
Root View (View1):
struct View1: View {
#State var showMenuButton: Bool = false
var body: some View {
VStack {
CustomNavigationView(showMenuButton: self.showMenuButton)
NavigationView {
NavigationLink(destination: View2()) {
Text("View 2")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onDisappear(){
self.showMenuButton = false
}
.onAppear() {
self.showMenuButton = true
}
}
}
}
}
Child View of root view (View2):
struct View2: View {
var body: some View {
VStack{
Text("This is View 2")
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: View3()) {
Text("View 3")
}
}
}
}
Child view of view 2 (View3):
struct View3: View {
var body: some View {
VStack{
Text("This is View 3")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
Custom Navigation View:
struct CustomNavigationView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var showMenuButton = false
var body: some View {
VStack {
HStack {
if showMenuButton {
Button(action: {
//Do Something
}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.black)
}
} else {
Button(action: { self.presentationMode.wrappedValue.dismiss()}) {
Image(systemName: "arrow.left")
.foregroundColor(.black)
}
}
Text("Sometext")
}
}
}
}
The environment object 'presentationMode' that you used inside the first view cannot dismiss the views you pushed. Every view that wants to be dismissed must have their own objects. The object inside the first view does not belong to any other pushed views. So, you need to create view model to manage this task.
Here is the example code. Hope that will help you solve your problem.
class NavigationObserver: ObservableObject {
private var views: [Int:Binding<PresentationMode>] = [:]
private var current: Int = 0
func popView() {
guard let view = views[current] else {
return
}
view.wrappedValue.dismiss()
views[current] = nil
current -= 1
}
func pushView(id: Int, newView: Binding<PresentationMode>) {
guard views[id] == nil else {
return
}
current += 1
views[id] = newView
}
}
struct ContentView: View {
#State var showMenuButton: Bool = false
#ObservedObject var observer = NavigationObserver()
var body: some View {
VStack {
CustomNavigationView(observer: self.observer, showMenuButton: self.showMenuButton)
NavigationView {
NavigationLink(destination: View2(observer: self.observer)) {
Text("View 2")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.onDisappear(){
self.showMenuButton = false
}
.onAppear() {
self.showMenuButton = true
}
}
}
}}
struct View2: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var observer: NavigationObserver
var body: some View {
VStack{
Text("This is View 2")
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: View3(observer: self.observer)) {
Text("View 3")
}
}.onAppear {
self.observer.pushView(id: 1, newView: self.presentationMode)
}
}}
struct View3: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var observer: NavigationObserver
var body: some View {
VStack{
Text("This is View 3")
.navigationBarTitle("")
.navigationBarHidden(true)
}.onAppear
{
self.observer.pushView(id: 2, newView: self.presentationMode)
}
}
}
struct CustomNavigationView: View {
#ObservedObject var observer: NavigationObserver
var showMenuButton = false
var body: some View {
VStack {
HStack {
if showMenuButton {
Button(action: {
//Do Something
}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.black)
}
} else {
Button(action: {
self.observer.popView()
}) {
Image(systemName: "arrow.left")
.foregroundColor(.black)
}
}
Text("Sometext")
}
}
}
}
Thanks, X_X

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

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