How to pop SwiftUI NavigationLink view on changed condition (received from delegate)?

I have a SwiftUI view that is pushed on the navigation stack (using a

NavigationLink
).


This view is a

UIViewRepresentable
-wrapped
UIKit
-based view.


To receive delegate events from my

UIKit
-based view, there is a
Coordinater
class (which is instantiated in
makeCoordinator()
).


I now want to dismiss my SwiftUI view when one of the delegate method calls indicates a certain condition.


The only way I know to dismiss a

NavigationLink
-pushed-view is via:
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

But when I invoke in the my implementation of a delegate method (i.e., inside my

Coordinator
):
    self.mySwiftUI.presentationMode.wrappedValue.dismiss()

this result in a crash:

Thread 1: Fatal error: Reading Environment<Binding<PresentationMode>> outside View.body
.


Knowing that SwiftUI is a declarative way of creating UIs, how do I ever get this

dismiss()
function called? Is there a way to call a function when a
@State
variable changes for example?

The ultimate issue here, I believe, is a difference between how the environment is used with regular

View
types vs.
UIViewRepresentable
and friends. In the former you use the
@Environment
property wrapper to do things because the expectation is that everything is being done within the body call, and this seems to be borne out even when looking at, say, the action methods for
Button
s (because you can certainly use the presentation mode from an
@Environment
variable there—likely it's being called from the button's
body
method as a result of the touch handling (maybe on un-highlight, which changes the appearance of the button, at a guess).


Anyway, the important part:

UIViewRepresentable
updates its content in the
updateUIView(_:context:)
method, and one thing that context provides is the current environment. This one can be accessed freely, at least within this method, as any state management has been taken care of by the framework at this point. That isn't necessarily the case for regular
View
types, where I'm guessing the environment isn't expected to be in any particular state outside of the view rendering pipeline.


So, you can do what you need with two things:


  1. Replace your
    @Environment
    variable with an
    @State
    variable which is a boolean, set to
    false
    . Call it
    shouldDismiss
    or suchlike.
  2. In your
    updateUIView(_:context:)
    method, check the value of that state variable. If
    true
    , call
    context.environment.presentationMode.wrappedValue.dismiss()
    , otherwise do the usual thing. Depending on the size of your update method, maybe use a
    guard
    expression to do this, calling
    dismiss()
    and returning immediately.


Here's a rough sketch of what you can use:


struct MyUIView: UIViewRepresentable {
     @State fileprivate var shouldDismiss = false

     func makeCoordinator() -> Coordinator { Coordinator(self) }

     func makeUIView(context: Context) -> UIViewType {
          // ...
     }

     func updateUIView(_ uiView: UIViewType, context: Context) {
          guard !shouldDismiss || !context.environment.presentationMode.wrappedValue.isPresented else {
               context.environment.presentationMode.wrappedValue.dismiss()
               return
          }

          // ...
     }

     class Coordinator: NSObject, MyViewDelegate {
          var parent: MyUIView
          init(parent: MyUIView) {
               self.parent = parent
          }
          func underlyingViewIsDone(_ view: UIView) {
               parent.shouldDismiss = true
          }
     }
}
How to pop SwiftUI NavigationLink view on changed condition (received from delegate)?
 
 
Q