I am trying to navigate between two views without a NavigationLink.
Here is the code:
import SwiftUI
struct NextView: View {
#State var text = ""
#Binding var displayView: Bool
var body: some View {
// 3
//NavigationView {
VStack {
Spacer()
TextField("Type something!", text: $text)
Button("Remove View") {
withAnimation {
displayView = false
}
}
Spacer()
}.background(Color.cyan)
//}
}
}
struct InitialView: View {
#State var displayView = false
var body: some View {
// 1
//NavigationView {
if displayView {
//2
//NavigationView {
NextView(displayView: $displayView)
.transition(.slide)
//}
} else {
Button("Tap to continue") {
withAnimation {
displayView = true
}
}
}
//}
}
}
I tried to place the NavigationView in 3 different places, one at a time. I managed to get the animation I wanted only by placing the structure in position 3 or not using it. Could anyone tell me why this happens and other possible solutions to use the NavigationView in position 1?
I tested only in the iPhone 12 simulator and am using XCode Version 13.4.1.
I think you just wanted this - animatable transition
struct InitialView: View {
#State var displayView = false
var body: some View {
NavigationView { // << just root view, not important
ZStack { // << owner container !!
if displayView {
NextView(displayView: $displayView)
.transition(.slide)
} else {
Button("Tap to continue") {
displayView = true
}
}
}
.animation(.default, value: displayView) // << animation !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
This is a tricky question. If you look closely at your code, you see that in postion 3 .transition is applied to the NavigationView. However, in positions 1 & 2 it’s not.
Transitions must be applied to the View that is transitioning, in your case, it’s the NavigationView that is wrapping the content. Implying that the NavigationView needs the modifier.
Add the transition to the NavigationView in any position & it should work as expected.
Related
In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context.
This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).
I didn't notice this behaviour in iOS 13.
Is there a way to 'deselect' the highlighted row once the other view is dismissed?
Example code:
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("Detail")) {
Text("Link")
}
}
}
}
(Different) Example visual:
In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
Putting the Text() beneath the List does not make any problems:
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("Does work correct")) {
Text("Does work correct")
}
}
Text("This text doesn't make problems.")
}
}
}
This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.
I have also run into this issue and believed I found the root cause in my case.
In my case I had.a structure like the following:
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
}
}
struct Page2View: View {
var body: some View {
List {
NavigationLink("Page 3", destination: Text("Page 3"))
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 2")
}
}
This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:
2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.
I.e.
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
.zIndex(1.0)
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.
Similar to what #pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.
This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.
I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.
Before (broken):
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)
After (working):
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
struct YourView: View {
#State private var selectedItem: String?
#State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}
I made a modifier based on this that you can use:
struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
#State private var viewId = UUID()
#Binding var selection: Tag?
func body(content: Content) -> some View {
content
.id(viewId)
.onAppear {
if selection != nil {
viewId = UUID()
selection = nil
}
}
}
}
extension View {
func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
}
}
use it like this:
List { ... }
.refreshOnAppear(selection: $selectedItem)
I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear
struct ContentView: View {
#State var selection: String? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
List {
Section {
NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
Text("Subscreen1")
}.onDisappear {
self.selection = nil
}
NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
Text("Subscreen2")
}.onDisappear {
self.selection = nil
}
}.id("idSection1")
}
.id("idList")
}
}
}
}
I've also run into this issue and it seemed related to sheets as mentioned here.
My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.
Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.
Suggested in this thread: https://developer.apple.com/forums/thread/660468
This is my solution to this issue.
// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View {
#State public var viewColor: Color?
public init(color: Color? = nil) {
self.viewColor = color
}
public var body: some View {
Rectangle()
.fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
}
}
// handy modifier..
extension List {
public func uncollapsing(_ viewUpdater: Bool) -> some View {
VStack(spacing: 0.0) {
Group {
PreventCollapseView()
self
}.id(viewUpdater)
}
}
}
struct TestView: View {
#State var updater: Bool = false
var body: some View {
List {
Text("Item one")
Text("Item two")
Text("Manually refresh")
.onTapGesture { DispatchQueue.main.async { updater.toggle() } }
.onAppear { print("List was refreshed") }
}
.uncollapsing(updater)
.clipped()
.onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing
}
}
Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.
Having the same Problem. The weird thing is, that the exact same code worked in iOS13.
I'm having this issue with a simple list:
struct TestList: View {
let someArray = ["one", "two", "three", "four", "five"]
var body: some View {
List(someArray, id: \.self) { item in
NavigationLink(
destination: Text(item)) {
Text(item)
}.buttonStyle(PlainButtonStyle())
}.navigationBarTitle("testlist")
}
}
This is embedded in:
struct ListControllerView: View {
#State private var listPicker = 0
var body: some View {
NavigationView{
Group{
VStack{
Picker(selection: $listPicker, label: Text("Detailoverview")) {
Text("foo").tag(0)
Text("bar").tag(1)
Text("TestList").tag(2)
}
This is inside a Tabbar.
This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.
import SwiftUI
import Introspect
struct MyView: View {
#State var reload: (() -> Void)? = nil
var body: some View {
NavigationView {
List {
NavigationLink("Next", destination: Text("Hello"))
}.introspectTableView { tv in
self.reload = tv.reloadData
}.onAppear {
self.reload?()
}
}
}
}
I want to achieve the function. Like, "Look up" view that is from Apple.
My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.
I try to achieve this function.
I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.
This is my code.
import SwiftUI
struct ContentView: View {
#State var isShowSheet = false
var body: some View {
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Tap to show the sheet")
}.sheet(isPresented: $isShowSheet) {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView()) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
)
}
}
How did I achieve this function? :)
Please help me, thank you. :)
UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var showingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
NavView()
.environment(\.showingSheet, self.$isShowSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.showingSheet) var showingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
self.showingSheet?.wrappedValue = false
}
)
}
}
I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.
One way I learned just now is use a #Binding var. This might not be the best solution, but it worked!
So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.
ContentView
.....
}.sheet(isPresented: $isShowSheet) {
NavView(isShowSheet: self.$isShowSheet)
}
NavView
struct NavView: View {
#Binding var isShowSheet: Bool
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
and finally, do the same thing to your subView.
NavSubView
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isShowSheet: Bool
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
//self.presentationMode.projectedValue.wrappedValue.dismiss()
self.isShowSheet = false
}){
Text("Done")
}
)
}
}
Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.
self.isShowSheet = false
Voila!
Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 13 / iOS 15)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var isShowingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
#State var isShowingSheet = false
...
Button("open sheet") {
isShowingSheet?.wrappedValue = true
}
// note no $ in front of isShowingSheet
.sheet(isPresented: isShowingSheet ?? .constant(false)) {
NavView()
.environment(\.isShowingSheet, self.$isShowingSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.isShowingSheet) var isShowingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
isShowingSheet?.wrappedValue = false
}
)
}
}
Here is a bug in SwiftUI when you show modal from button inside navigation bar items.
In code below Button 1 works as expected, but Button 2 works only once:
struct DetailView: View {
#Binding var isPresented: Bool
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Text("OK")
.navigationBarTitle("Details")
.navigationBarItems(trailing: Button(action: {
self.isPresented = false
// or:
// self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done").bold()
})
}
}
}
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold()
})
}.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
}
}
}
This bug is from the middle of the last year, and it still in Xcode 11.3.1 + iOS 13.3 Simulator and iOS 13.3.1 iPhone XS.
Is here any workaround to make button work?
EDIT:
Seems to be tap area goes somewhere down and it's possible to tap below button to show modal.
Temporary solution to this is to use inline navigation bar mode:
.navigationBarTitle("Main", displayMode: .inline)
Well, the issue is in bad layout (seems broken constrains) of navigation bar button after sheet has closed
It is clearly visible in view hierarchy debug:
Here is a fix (workaround of course, but safe, because even after issue be fixed it will continue working). The idea is not to fight with broken layout but just create another button, so layout engine itself remove old-bad button and add new one refreshing layout. The instrument for this is pretty known - use .id()
So modified code:
struct ContentView: View {
#State var showSheetView = false
#State private var navigationButtonID = UUID()
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold() // recommend .padding(.vertical) here
}
.id(self.navigationButtonID)) // force new instance creation
}
.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
.onDisappear {
// update button id after sheet got closed
self.navigationButtonID = UUID()
}
}
}
}
I'm trying to create a really simple transition animation that shows/hides a message in the center of the screen by tapping on a button:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
According to the documentation of the .transition(.opacity) animation
A transition from transparent to opaque on insertion, and from opaque
to transparent on removal.
the message should fade in when the showMessage state property becomes true and fade out when it becomes false. This is not true in my case. The message shows up with a fade animation, but it hides with no animation at all. Any ideas?
EDIT: See the result in the gif below taken from the simulator.
The problem is that when views come and go in a ZStack, their "zIndex" doesn't stay the same. What is happening is that the when "showMessage" goes from true to false, the VStack with the "Hello World" text is put at the bottom of the stack and the yellow color is immediately drawn over top of it. It is actually fading out but it's doing so behind the yellow color so you can't see it.
To fix it you need to explicitly specify the "zIndex" for each view in the stack so they always stay the same - like so:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow.zIndex(0)
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}.zIndex(1)
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
.zIndex(2)
}
}
}
}
My findings are that opacity transitions don't always work. (yet a slide in combination with an .animation will work..)
.transition(.opacity) //does not always work
If I write it as a custom animation it does work:
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
.zIndex(1)
I found a bug in swiftUI_preview for animations. when you use a transition animation in code and want to see that in SwiftUI_preview it will not show animations or just show when some views disappear with animation. for solving this problem you just need to add your view in preview in a VStack. like this :
struct test_UI: View {
#State var isShowSideBar = false
var body: some View {
ZStack {
Button("ShowMenu") {
withAnimation {
isShowSideBar.toggle()
}
}
if isShowSideBar {
SideBarView()
.transition(.slide)
}
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SomeView()
}
}
}
after this, all animations will happen.
I believe this is a problem with the canvas. I was playing around with transitions this morning and while the don't work on the canvas, they DO seem to work in the simulator. Give that a try. I've reported the bug to Apple.
I like Scott Gribben's answer better (see below), but since I cannot delete this one (due to the green check), I'll just leave the original answer untouched. I would argue though, that I do consider it a bug. One would expect the zIndex to be implicitly assigned by the order views appear in code.
To work around it, you may embed the if statement inside a VStack.
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
VStack {
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
}
zIndex may cause the animation to be broken when interrupted. Wrap the view you wanna apply transition to in a VStack, HStack or any other container will make sense.
I just gave up on .transition. It's just not working. I instead animated the view's offset, much more reliable:
First I create a state variable for offset:
#State private var offset: CGFloat = 200
Second, I set the VStack's offset to it. Then, in its .onAppear(), I change the offset back to 0 with animation:
VStack{
Spacer()
HStack{
Spacer()
Image("MyImage")
}
}
.offset(x: offset)
.onAppear {
withAnimation(.easeOut(duration: 2.5)) {
offset = 0
}
}
Below code should work.
import SwiftUI
struct SwiftUITest: View {
#State private var isAnimated:Bool = false
var body: some View {
ZStack(alignment:.bottom) {
VStack{
Spacer()
Button("Slide View"){
withAnimation(.easeInOut) {
isAnimated.toggle()
}
}
Spacer()
Spacer()
}
if isAnimated {
RoundedRectangle(cornerRadius: 16).frame(height: UIScreen.main.bounds.height/2)
.transition(.slide)
}
}.ignoresSafeArea()
}
}
struct SwiftUITest_Previews: PreviewProvider {
static var previews: some View {
VStack {
SwiftUITest()
}
}
}
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")
})