Picker selection within navigation link causes strange behaviour - swift

I have a picker embedded in a form on a screen within a navigation view stack. I've re-created a simplistic version.
struct ContentView: View {
#State var showSecondView: Bool = false
var body: some View {
NavigationView {
VStack {
Button("SecondView", action: {
self.showSecondView = true
})
NavigationLink(destination: SecondContentView(), isActive: $showSecondView) {
EmptyView()
}
}
}
}
}
struct SecondContentView: View {
#State var showThirdView: Bool = false
var body: some View {
VStack {
Button("ThirdView", action: {
self.showThirdView = true
})
NavigationLink(destination: ThirdContentView(showThirdView: $showThirdView), isActive: $showThirdView) {
EmptyView()
}
}
}
}
struct ThirdContentView: View {
#Binding var showThirdView: Bool
#State var pickerSelection: String = ""
let pickerObjects = ["A", "B", "C"]
var body: some View {
VStack {
Form {
Picker(selection: $pickerSelection, label: Text("Abort Reason")
) {
ForEach(0 ..< pickerObjects.count) { i in
Text("\(self.pickerObjects[i])").tag(self.pickerObjects[i])
}
}
}
Button("Done", action: {
self.showThirdView.toggle()
})
}
}
}
In the example above when I set a value and press done it navigates back to the third screen (with the picker) but without a value selected. In my full app pressing done dismisses the third screen but then when I press back on the second screen it briefly shows the third screen for a second before dismissing it.
If I present the third view outside of a navigation link (if showThirdView == true) then no navigation errors. The setting of a value in the picker seems to add another instance of the third view to the NavigationView stack rather than going back. I like the navigation link style as the back button is consistent for the user. Is there any way to get the picker to work within a navigation link?

Here is fixed parts that works - replaced Binding, which becomes lost, with presentation mode. Tested with Xcode 12 / iOS 14.
struct SecondContentView: View {
#State var showThirdView: Bool = false
var body: some View {
VStack {
Button("ThirdView", action: {
self.showThirdView = true
})
NavigationLink(destination: ThirdContentView(), isActive: $showThirdView) {
EmptyView()
}
}
}
}
struct ThirdContentView: View {
#Environment(\.presentationMode) var mode
#State var pickerSelection: String = ""
let pickerObjects = ["A", "B", "C"]
var body: some View {
VStack {
Form {
Picker(selection: $pickerSelection, label: Text("Abort Reason")
) {
ForEach(0 ..< pickerObjects.count) { i in
Text("\(self.pickerObjects[i])").tag(self.pickerObjects[i])
}
}
}
Button("Done", action: {
self.mode.wrappedValue.dismiss()
})
}
}
}

Related

SwiftUI deep linking with NavigationLink inside List onAppear with tag: and selection: doesn't activate link

Trying to build deep linking into a list of NavigationList items; I will be reading a value on the SwiftUI view's .onAppear and based on that value, navigate to a specific cell. There are three issues that come up with different setups I have tried: (1) with the below code, navigation doesn't happen at all, (2) if it does navigate, it will immediately pop back, (3) if programmatic navigation works and it doesn't pop back, the manual navigation doesn't work.
I have tried this with a Binding dictionary, and I get issue #2 above. Not only this, but in both solutions, user has to scroll to the cell in order to even read the binding/selection.
import SwiftUI
struct ContentViewTwo: View {
var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
}
#State var selected: String?
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, tag: data, selection: self.$selected) {
Text("Tap for Page 3 with Data: \(data):")
}.onAppear() {
print("link appeared.")
}
}.onAppear() {
if (self.shouldPushPage3) {
self.selected = "3" // Has no affect. 😢
self.shouldPushPage3 = false
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Tap for Page 2")
}
}
}
}
}
You need to dispatch the selection.
.onAppear {
guard shouldPushPage3 else { return }
shouldPushPage3 = false
DispatchQueue.main.async {
selection = "3"
}
}

Dynamic NavigationLink destination in SwiftUI?

I have a View 'B' that has an initialiser that takes an argument.
struct B: View {
let arg: Int
init(arg: Int) {
self.arg = arg
}
var body: some View {
Text("\(arg)")
}
}
And I have a Navigation View 'A'.
'A' has one button, which when pressed, shows a popup where the user picks a number from 1-5. A closure of type (Int) -> Void is called with the chosen number.
struct A: View {
#State var showPicker = false
var body: some View {
NavigationView {
Button(action: { self.showPicker = true }) {
Text("Pick Number")
}
.sheet(isPresented: self.$showPicker, content: {
NumberPicker { number in
*** Possible to navigate to B from here? ***
}
})
}
}
}
Question
Is it possible to initialise view B with the result from the closure, and then navigate to it?
This used to be possible with DynamicNavigationDestinationLink, however Apple deprecated it and stated in the release notes that NavigationLink contains its capabilities now. I have searched through the docs, however, I have not been able to figure out how to use NavigationLink to produce the same outcome.
You need to add a new view with another navigationview where you can pick the number.
Here's a full code example how i would approach this:
import SwiftUI
struct viewA: View {
var body: some View {
NavigationView {
NavigationLink(destination: viewC()) {
Text("Pick Number")
}
}
}
}
struct viewB: View {
#Binding var showSheet: Bool
#Binding var arg: Int
var body: some View {
NavigationView {
List {
ForEach((1...5), id: \.self) {number in
Button(action: {
self.arg = number
self.showSheet.toggle()
}) {
Text("\(number)")
}
}
}
}
}
}
struct viewC: View {
#State var showSheet:Bool = true
#State var arg: Int = 0
var body: some View {
NavigationView {
Text("\(arg)")
}
.sheet(isPresented: $showSheet) {
viewB(showSheet: self.$showSheet, arg: self.$arg)
}
}
}

SwiftUI - Form with error message on button press and navigation

I have the following scenario. I have a text field and a button, what I would need is to show an error message in case the field is empty and if not, navigate the user to the next screen.
I have tried showing the error message conditionally by using the field value and checking if it is empty on button press, but then, I don't know how to navigate to the next screen.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Error, please enter value")
}
Button(action: {
if self.fieldValue.isEmpty {
self.showErrorMessage = true
} else {
self.showErrorMessage = false
//How do I put navigation here, navigation link does not work, if I tap, nothing happens
}
}) {
Text("Next")
}
}
}
}
}
Using UIKit would be easy since I could use self.navigationController.pushViewController
Thanks to part of an answer here, here's some working code.
First, I moved everything into an EnvronmentObject to make things easier to pass to your second view. I also added a second toggle variable:
class Model: ObservableObject {
#Published var fieldValue = ""
#Published var showErrorMessage = false
#Published var showSecondView = false
}
Next, change two things in your ContentView. I added a hidden NavigationLink (with a isActive parameter) to actually trigger the push, along with changing your Button action to execute a local function:
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $model.fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
NavigationLink(destination: SecondView(), isActive: $model.showSecondView) {
Text("NavLink")
}.hidden()
Button(action: {
self.checkForText()
}) {
Text("Next")
}
.alert(isPresented: self.$model.showErrorMessage) {
Alert(title: Text("Error"), message: Text("Please enter some text!"), dismissButton: .default(Text("OK")))
}
}
}
}
func checkForText() {
if model.fieldValue.isEmpty {
model.showErrorMessage.toggle()
} else {
model.showSecondView.toggle()
}
}
}
Toggling showErrorMessage will show the Alert and toggling `showSecondView will take you to the next view.
Finally, the second view:
struct SecondView: View {
#EnvironmentObject var model: Model
var body: some View {
ZStack {
Rectangle().fill(Color.green)
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.showSecondView = false
})
Text(model.fieldValue)
}
}
func popSecondView() {
model.showSecondView.toggle()
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
This is where the above linked answer helped me. It appears there's a bug in navigation back that still exists in beta 6. Without this workaround (that toggles showSecondView) you will get sent back to the second view one more time.
You didn't post any details on the second view contents, so I took the liberty to add someText into the model to show you how to easily pass things into it can be using an EnvironmentObject. There is one bit of setup needed to do this in SceneDelegate:
var window: UIWindow?
var model = Model()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(model))
self.window = window
window.makeKeyAndVisible()
}
}
I noticed a slight change in this, depending on when your project was created (beta 6 declares an instance of contentView where older versions do not). Either way, declare an instance of model and then add the envoronmentObject modifier to contentView.
Another approach is to make the "Next" button conditionally a Button when the fieldValue is empty and a NavigationLink when the fieldValue is valid. The Button case will trigger your error message view and the NavigationLink will do the navigation for you. Keeping this close to your sample, the following seems to do the trick.
struct SomeView: View {
#State var fieldValue = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("My Field", text: $fieldValue).textFieldStyle(RoundedBorderTextFieldStyle())
if showErrorMessage {
Text("Please Enter Data")
}
if fieldValue == "" {
Button(action: {
if self.fieldValue == "" {
self.showErrorMessage = true
}
}, label: {
Text("Next")
})
} else {
// move on case
NavigationLink("Next", destination: Text("Next View"))
}
}
}
}
}
By using this code we can display the alert if the fields are empty else . it will navigate.
struct SomeView: View {
#State var userName = ""
#State var password = ""
#State var showErrorMessage = false
var body: some View {
NavigationView {
VStack {
TextField("Enter Username", text: $userName).textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Enter Your Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if userName == "" || password == "" {
Button(action: {
if self.userName == "" || self.password == "" {
self.showErrorMessage = true
}
}, label: {
Text("Login")
})
} else {
// move case
NavigationLink("Login", destination: Text("Login successful"))
}
}.alert(isPresented: $showErrorMessage) { () -> Alert in
Alert(title: Text("Important Message"), message: Text("Please Fill all the Fields"), primaryButton: .default(Text("Ok")), secondaryButton: .destructive(Text("Cancel")))
}
}
}
}

SwiftUI: Support multiple modals

I'm trying to setup a view that can display multiple modals depending on which button is tapped.
When I add just one sheet, everything works:
.sheet(isPresented: $showingModal1) { ... }
But when I add another sheet, only the last one works.
.sheet(isPresented: $showingModal1) { ... }
.sheet(isPresented: $showingModal2) { ... }
UPDATE
I tried to get this working, but I'm not sure how to declare the type for modal. I'm getting an error of Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
struct ContentView: View {
#State var modal: View?
var body: some View {
VStack {
Button(action: {
self.modal = ModalContentView1()
}) {
Text("Show Modal 1")
}
Button(action: {
self.modal = ModalContentView2()
}) {
Text("Show Modal 2")
}
}.sheet(item: self.$modal, content: { modal in
return modal
})
}
}
struct ModalContentView1: View {
var body: some View {
Text("Modal 1")
}
}
struct ModalContentView2: View {
var body: some View {
Text("Modal 2")
}
}
This works:
.background(EmptyView().sheet(isPresented: $showingModal1) { ... }
.background(EmptyView().sheet(isPresented: $showingModal2) { ... }))
Notice how these are nested backgrounds. Not two backgrounds one after the other.
Thanks to DevAndArtist for finding this.
Maybe I missed the point, but you can achieve it either with a single call to .sheet(), or multiple calls.:
Multiple .sheet() approach:
import SwiftUI
struct MultipleSheets: View {
#State private var sheet1 = false
#State private var sheet2 = false
#State private var sheet3 = false
var body: some View {
VStack {
Button(action: {
self.sheet1 = true
}, label: { Text("Show Modal #1") })
.sheet(isPresented: $sheet1, content: { Sheet1() })
Button(action: {
self.sheet2 = true
}, label: { Text("Show Modal #2") })
.sheet(isPresented: $sheet2, content: { Sheet2() })
Button(action: {
self.sheet3 = true
}, label: { Text("Show Modal #3") })
.sheet(isPresented: $sheet3, content: { Sheet3() })
}
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
Single .sheet() approach:
struct MultipleSheets: View {
#State private var showModal = false
#State private var modalSelection = 1
var body: some View {
VStack {
Button(action: {
self.modalSelection = 1
self.showModal = true
}, label: { Text("Show Modal #1") })
Button(action: {
self.modalSelection = 2
self.showModal = true
}, label: { Text("Show Modal #2") })
Button(action: {
self.modalSelection = 3
self.showModal = true
}, label: { Text("Show Modal #3") })
}
.sheet(isPresented: $showModal, content: {
if self.modalSelection == 1 {
Sheet1()
}
if self.modalSelection == 2 {
Sheet2()
}
if self.modalSelection == 3 {
Sheet3()
}
})
}
}
struct Sheet1: View {
var body: some View {
Text("This is Sheet #1")
}
}
struct Sheet2: View {
var body: some View {
Text("This is Sheet #2")
}
}
struct Sheet3: View {
var body: some View {
Text("This is Sheet #3")
}
}
I'm not sure whether this was always possible, but in Xcode 11.3.1 there is an overload of .sheet() for exactly this use case (https://developer.apple.com/documentation/swiftui/view/3352792-sheet). You can call it with an Identifiable item instead of a bool:
struct ModalA: View {
var body: some View {
Text("Hello, World! (A)")
}
}
struct ModalB: View {
var body: some View {
Text("Hello, World! (B)")
}
}
struct MyContentView: View {
enum Sheet: Hashable, Identifiable {
case a
case b
var id: Int {
return self.hashValue
}
}
#State var activeSheet: Sheet? = nil
var body: some View {
VStack(spacing: 42) {
Button(action: {
self.activeSheet = .a
}) {
Text("Hello, World! (A)")
}
Button(action: {
self.activeSheet = .b
}) {
Text("Hello, World! (B)")
}
}
.sheet(item: $activeSheet) { item in
if item == .a {
ModalA()
} else if item == .b {
ModalB()
}
}
}
}
I personally would mimic some NavigationLink API. Then you can create a hashable enum and decide which modal sheet you want to present.
extension View {
func sheet<Content, Tag>(
tag: Tag,
selection: Binding<Tag?>,
content: #escaping () -> Content
) -> some View where Content: View, Tag: Hashable {
let binding = Binding(
get: {
selection.wrappedValue == tag
},
set: { isPresented in
if isPresented {
selection.wrappedValue = tag
} else {
selection.wrappedValue = .none
}
}
)
return background(EmptyView().sheet(isPresented: binding, content: content))
}
}
enum ActiveSheet: Hashable {
case first
case second
}
struct First: View {
var body: some View {
Text("frist")
}
}
struct Second: View {
var body: some View {
Text("second")
}
}
struct TestView: View {
#State
private var _activeSheet: ActiveSheet?
var body: some View {
print(_activeSheet as Any)
return VStack
{
Button("first") {
self._activeSheet = .first
}
Button("second") {
self._activeSheet = .second
}
}
.sheet(tag: .first, selection: $_activeSheet) {
First()
}
.sheet(tag: .second, selection: $_activeSheet) {
Second()
}
}
}
I wrote a library off plivesey's answer that greatly simplifies the syntax:
.multiSheet {
$0.sheet(isPresented: $sheetAPresented) { Text("Sheet A") }
$0.sheet(isPresented: $sheetBPresented) { Text("Sheet B") }
$0.sheet(isPresented: $sheetCPresented) { Text("Sheet C") }
}
I solved this by creating an observable SheetContext that holds and manages the state. I then only need a single context instance and can tell it to present any view as a sheet. I prefer this to the "active view" binding approach, since you can use this context in multiple ways.
I describe it in more details in this blog post: https://danielsaidi.com/blog/2020/06/06/swiftui-sheets
I think i found THE solution. It's complicated so here is the teaser how to use it:
Button(action: {
showModal.wrappedValue = ShowModal {
AnyView( TheViewYouWantToPresent() )
}
})
Now you can define at the button level what you want to present. And the presenting view does not need to know anything. So you call this on the presenting view.
.background(EmptyView().show($showModal))
We call it on the background so the main view does not need to get updated, when $showModal changes.
Ok so what do we need to get this to work?
1: The ShowModal class:
public enum ModalType{
case sheet, fullscreen
}
public struct ShowModal: Identifiable {
public let id = ""
public let modalType: ModalType
public let content: () -> AnyView
public init (modalType: ModalType = .sheet, #ViewBuilder content: #escaping () -> AnyView){
self.modalType = modalType
self.content = content
}
}
Ignore id we just need it for Identifiable. With modalType we can present the view as sheet or fullscreen. And content is the passed view, that will be shown in the modal.
2: A ShowModal binding which stores the information for presenting views:
#State var showModal: ShowModal? = nil
And we need to add it to the environment of the view thats responsible for presentation. So we have easy access to it down the viewstack:
VStack{
InnerViewsThatWantToPresentModalViews()
}
.environment(\.showModal, $showModal)
.background(EmptyView().show($showModal))
In the last line we call .show(). Which is responsible for presentation.
Keep in mind that you have to create #State var showModal and add it to the environment again in a view thats shown modal and wants to present another modal.
4: To use .show we need to extend view:
public extension View {
func show(_ modal: Binding<ShowModal?>) -> some View {
modifier(VM_Show(modal))
}
}
And add a viewModifier that handles the information passed in $showModal
public struct VM_Show: ViewModifier {
var modal: Binding<ShowModal?>
public init(_ modal: Binding<ShowModal?>) {
self.modal = modal
}
public func body(content: Content) -> some View {
guard let modalType = modal.wrappedValue?.modalType else{ return AnyView(content) }
switch modalType {
case .sheet:
return AnyView(
content.sheet(item: modal){ modal in
modal.content()
}
)
case .fullscreen:
return AnyView(
content.fullScreenCover(item: modal) { modal in
modal.content()
}
)
}
}
}
4: Last we need to set showModal in views that want to present a modal:
Get the variable with: #Environment(\.showModal) var showModal. And set it like this:
Button(action: {
showModal.wrappedValue = ShowModal(modalType: .fullscreen) {
AnyView( TheViewYouWantToPresent() )
}
})
In the view that defined $showModal you set it without wrappedValue: $showModal = ShowModal{...}
As an alternative, simply putting a clear pixel somewhere in your layout might work for you:
Color.clear.frame(width: 1, height: 1, alignment: .center).sheet(isPresented: $showMySheet, content: {
MySheetView();
})
Add as many pixels as necessary.

SwiftUI - Is there a popViewController equivalent in SwiftUI?

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
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.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State 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
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}