Presenting New Views in SwiftUI Without a Button - ios

So I want to present a new view using SwiftUI, without the user having to tap a button, since NavigationButton would work with that. Here is an example
struct ContentView : View {
var model: Model
var body: some View {
NavigationView {
Text("Hello World")
}.onAppear {
if model.shouldPresent {
// present a new view
}
}
}
}
In the onAppear I want to include some code that will push a new view onto the navigation stack.

Here's a way to present view as a Modal.
struct PresentOnloadView: View {
var body: some View {
HStack {
Text("Hey there")
}
.presentation(Modal(HelloView(), onDismiss: nil))
}
}
struct HelloView: View {
var body: some View {
Text("Whats up! 👻")
}
}
Similarly, if you're looking to control whether to present or not using a variable, you can do something like this..
struct PresentOnloadControlledView : View {
#State var sayHello = false
var body: some View {
HStack {
Text("What's up!")
}
.onAppear(perform: {
// Decide whether to show another view or not here
self.sayHello = true
})
.presentation(sayHello ? Modal(HelloView()) : nil)
}
}
As of Version 11.0 beta 4 ➝ .presentation and Modal has been deprecated.
Not to worry! .sheet saves the day!
struct PresentOnloadControlledView : View {
#State var sayHello = false
var body: some View {
HStack {
Text("What's up!")
}
.onAppear(perform: {
// Decide whether to show another view or not here
self.sayHello = true
})
.sheet(isPresented: $sayHello) {
HelloView()
}
}
}

SwiftUI 2
Segue equivalents in SwiftUI 2 / iOS 14:
Show (Push)
struct ContentView: View {
#State var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
.background(NavigationLink("", destination: Text("Second view"), isActive: $showSecondView))
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Show Detail (Replace)
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
if showSecondView {
Text("Second view")
} else {
Text("First view")
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Present Modally
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
}
.sheet(isPresented: $showSecondView) {
Text("Second view")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Present as Full Screen
struct ContentView: View {
#State private var showSecondView = false
var body: some View {
NavigationView {
Text("First view")
}
.fullScreenCover(isPresented: $showSecondView) {
Text("Second view")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showSecondView = true
}
}
}
}
Note: I added DispatchQueue.main.asyncAfter(deadline: .now() + 1) so you can see the transition. You can remove it and the transition will start immediately.

Inside your view:
#State var present = true
var body: some View {
NavigationLink(destination: DestinationView(), isActive: $present) {
EmptyView()
}
}
This code presents DestinationView as soon as the NavigationLink loads. The crucial part is the isActive parameter, which programmatically triggers the transition to the other View.
You could rewrite as follows
#State var present = false
var body: some View {
NavigationLink(destination: DestinationView(), isActive: $present) {
Button {
present = true
} label: { Text("Present") }
}
}
To manually create a navigation button.

You can simply put the logic inside the NavigationView. Like this:
#State var x = true;
var body : some View {
NavigationView {
if x {
Text("Hello")
Button(action: {
self.x = false;
}, label: { Text("Click Me") })
} else {
Text("World")
}
}
}

Related

Why does withAnimation not cause an animation?

I try to switch from one screen to another by pressing a button (see full code below). The switch from the first view to the second view (and vice versa) works, but no animation is taking place.
Why does this behavior happen?
Full code:
struct ContentView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
if isFirstViewActive {
FirstView()
} else {
SecondView()
}
}
}
struct FirstView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
ZStack {
Color(.red).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the first view")
Spacer()
Button {
withAnimation {
isFirstViewActive = false
}
} label: {
Text("Go to second view")
}
Spacer()
}
}
}
}
struct SecondView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = false
var body: some View {
ZStack {
Color(.blue).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the second view")
Spacer()
Button {
withAnimation {
isFirstViewActive = true
}
} label: {
Text("Go to first view")
}
Spacer()
}
}
}
}
The problem is with
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
If you change that to
#State var isFirstViewActive: Bool = true
and use #Binding in subviews, you will get the default animations.
In iOS 16, there seems to be a problem with #AppStorage vars and animation. But you can refer to this workaround

Navigating to New Views in a Split View (iPad)

Here is a split view:
If I click on the "Toggle", I'd like to see
And If I click Toggle again, show Left 1 - Right 1 again, and so on.
Here is my code:
struct ContentView2: View {
#State var toggle : String
var body: some View {
NavigationView {
if toggle == "first" {
LHSTest1()
RHSTest1()
}
else {
LHSTest2()
RHSTest2()
}
}
}
}
struct LHSTest1: View {
#State private var isActiveForLHS1 = false
var body: some View {
VStack {
Button("Toggle") {
self.isActiveForLHS1 = true
}
.padding()
Text("Left 1")
NavigationLink(destination: ContentView2(toggle: "second"), isActive: $isActiveForLHS1) { }.opacity(0)
}
}
}
struct RHSTest1: View {
var body: some View {
Text("Right 1")
}
}
struct LHSTest2: View {
#State private var isActiveForLHS2 = false
var body: some View {
VStack {
Button("Toggle") {
self.isActiveForLHS2 = true
}
Text("Left 2")
NavigationLink(destination: ContentView2(toggle: "first"), isActive: $isActiveForLHS2) { }.opacity(0)
}
}
}
struct RHSTest2: View {
var body: some View {
Text("Right 2")
}
}
Here is the problem: When I click toggle, a new layer of navigation view appears:
Any thoughts will be greatly appreciated.

Can I use a button inside a SwiftUI sheet view that appears on top of my main SwiftUI view to change a subview inside the main view?

I'm trying to use a pop up menu (that appears when the user triggers it) to make the users of my app able to change the subview that is shown inside my main view between a Subview1 and a Subview2.
I'm trying to do that using global Bool variables that are changed when a button in the view inside the sheet is pressed. Based on those values, the main view should return a different subview
The problem is that when I try to select one option from the view that appears inside the sheet, the action of the Button is performed and the sheet is dismissed but the subview displayed by the main view remains unchanged
Is there a way to change the subview or reload the main view?
The code I'm using for the main view is:
struct ContentView: View {
#State var showMenu = false
var body: some View {
if(subview1Selected){
return AnyView(SubView1())
} else if (subview2Selected){
return AnyView(SubView2())
}
else {
return AnyView(
Button(action: {
showMenu = true
})
{
Text("Button")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
)
}
}
the code I'm using for the pop-up sheet that is used like a menu is:
struct MenuView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
List{
Button(action: {
subview1Selected = true
subview2Selected = false
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Subview1")
}
Button(action: {
subview2Selected = true
subview1Selected = false
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Subview2")
}
}
}
}
The subviews are:
struct SubView1: View {
#State var showMenu = false
var body: some View {
Button(action: {
showMenu = true
})
{
Text("SubView1")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
}
}
and:
struct SubView2: View {
#State var showMenu = false
var body: some View {
Button(action: {
showMenu = true
})
{
Text("SubView2")
}
.sheet(isPresented: $showMenu, content: {
MenuView()
})
}
}
I think this is what you are trying to do. You can pass the #State showMenu variable as a #Binding variable into the menu. You can use a Bool if you only have 2 views, but it's more practical to use a custom enum, which I added for you. Also, the menu button should probably be separate from the subviews.
struct ContentView: View {
enum SubViewOption {
case subview1
case subview2
}
#State var showMenu = false
#State var subviewSelected: SubViewOption?
var body: some View {
ZStack() {
switch subviewSelected {
case .subview1:
SubView1()
case .subview2:
SubView2()
default:
Text("Select a view to begin.")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(
Button(action: {
showMenu.toggle()
}, label: {
Text("Menu")
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.gray.cornerRadius(30))
.padding()
})
, alignment: .bottom
)
.sheet(isPresented: $showMenu, content: {
MenuView(subviewSelected: $subviewSelected)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MenuView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var subviewSelected: ContentView.SubViewOption?
var body: some View {
List {
Button(action: {
subviewSelected = .subview1
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Subview1")
})
Button(action: {
subviewSelected = .subview2
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Subview2")
})
}
}
}
struct SubView1: View {
var body: some View {
ZStack {
Color.red
.edgesIgnoringSafeArea(.all)
Text("THIS IS SUBVIEW 1")
.foregroundColor(.white)
}
}
}
struct SubView2: View {
var body: some View {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
Text("THIS IS SUBVIEW 2")
.foregroundColor(.white)
}
}
}

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

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