How to pop a UIViewControllerRepresentable from a SwiftUI navigation stack programmatically - swift

Here is my SwiftUI setup:
I have a SwiftUI view (called MapSearchView) that has a NavigationView that contains a NavigationLink.
var body: some View {
VStack {
NavigationView {
Form {
Section (header: Text("Location")){
NavigationLink (locationString, destination: GooglePlacesViewController(mapTools: $mapTools))
}...
The NavigationLink's destination is a UIViewControllerRepresentable which wraps a GooglePlacesViewController. When I am done with my GooglePlaces search (either because I made a selection or I cancelled the search), I want to programmatically pop back to the MapSearchView.
I have tried adding the following in the UIViewControllerRepresentable (GooglePlacesViewController):
#Environment(\.presentationMode) var presentationMode
var popToPrevious : Bool = false {
didSet {
if popToPrevious == true {
self.presentationMode.wrappedValue.dismiss()
}
}
}
and setting the popToPrevious to true in my Coordinator using
parent.popToPrevious = true
It does get called, but does nothing.
I have also tried in the Coordinator to call
view.viewController().navigationController.popViewController(animated: true)
(viewController() is an extension that gets the viewController for any view)
This also does nothing.
Ideas?

If you create Coordinator with owner view (ie. your representable), then try to call directly when needed
ownerView.presentationMode.wrappedValue.dismiss()

Related

Run action when view is 'removed'

I am developing an app which uses UIKit. I have integrated a UIKit UIViewController inside SwiftUI and everything works as expected. I am still wondering if there is a way to 'know' when a SwiftUI View is completely gone.
My understanding is that a #StateObject knows this information. I now have some code in the deinit block of the corresponding class of the StateObject. There is some code running which unsubscribes the user of that screen.
The problem is that it is a fragile solution. In some scenario's the deinit block isn't called.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)? I don't want to get notified with the .onDisppear modifier because that is also called when the user taps somewhere on the screen which adds another view to the navigation stack. I want to run some code once when the screen is completely gone.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)?
This implies you're using a NavigationView and presenting your view with a NavigationLink.
You can be notified when the user goes “back” from your view by using one of the NavigationLink initializers that takes a Binding. Create a custom binding and in its set function, check whether the old value is true (meaning the child view was presented) and the new value is false (meaning the child view is now being popped from the stack). Example:
struct ContentView: View {
#State var childIsPresented = false
#State var childPopCount = 0
var body: some View {
NavigationView {
VStack {
Text("Child has been popped \(childPopCount) times")
NavigationLink(
"Push Child",
isActive: Binding(
get: { childIsPresented },
set: {
if childIsPresented && !$0 {
childPopCount += 1
}
childIsPresented = $0
}
)
) {
ChildView()
}
}
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Sweet child o' mine")
NavigationLink("Push Grandchild") {
GrandchildView()
}
}
}
}
struct GrandchildView: View {
var body: some View {
VStack {
Text("👶")
.font(.system(size: 100))
}
}
}
Note that these initializers, and NavigationView, are deprecated if your deployment target is iOS 16. In that case, you'll want to use a NavigationStack and give it a custom Binding that performs the pop-detection.

SwiftUI: resetting TabView

I have a TabView with two tabs in a SwiftUI lifecycle app, one of them has complex view structure: NavigationView with a lot of sub-views inside, i.e.: NavigationLinks and their DestinationViews are spread on multiple levels down the view tree, each sub-view on its own is another view hierarchy with sheets and / or other DestinationViews. At some point inside this hierarchy, I want to reset the TabView to its original state which is displaying the first most view, so the user can restart their journey right at that state, as they were to open the app for the first time, so it's kinda impossible to track down something like isActive & isPresented bindings to pop-off or dismiss the views and sheets.
I thought of wrapping the TabView inside another view: RootView in an attempt to find an easy way to recreate that TabView from scratch or something like refreshing / resetting the TabView, but couldn't find a clew on how to do it.
Here's my code snippet:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
struct RootView: View {
var body: some View {
ContentView()
}
}
struct ContentView: View {
var body: some View {
TabView { // <-- I need to reset it to its original state
View1() // <---- this view has complex view hierarchy
.tabItem {
Text("Home")
}.tag(0)
View2()
.tabItem {
Text("Settings")
}.tag(1)
}
}
}
p.s. I'm not looking for "popping off the view to root view", because this can't be done when there are many active NavigationLink destinations where the user might open one of the sheets and start a new navigation journey inside the sheet.
****** UPDATE ******
I've created a new Environment value to hold a boolean that should indicate whether the TabView should reset or not, and I've tracked every isPresented and isActive state variables in every view and reset them once that environment value is set to true like this:
struct ResetTabView: EnvironmentKey {
static var defaultValue: Binding<ResetTabObservable> = .constant(ResetTabObservable())
}
extension EnvironmentValues {
var resetTabView: Binding<ResetTabObservable> {
get { self[ResetTabView.self] }
set { self[ResetTabView.self] = newValue }
}
}
class ResetTabObservable: ObservableObject {
#Published var newValue = false
}
in every view that will present a sheet or push a new view I added something like this:
struct View3: View {
#State var showSheet = false
#Environment(\.resetTabView) var reset
var body: some View {
Text("This is view 3")
Button(action: {
showSheet = true
}, label: {
Text("show view 4")
})
.sheet(isPresented: $showSheet) {
View4()
}
.onReceive(reset.$newValue.wrappedValue, perform: { val in
if val == true {
showSheet = false
}
})
}
}
and in the last view (which will reset the TabView) I toggle the Environment value like this:
struct View5: View {
#Environment(\.resetTabView) var reset
var body: some View {
VStack {
Text("This is view 5")
Button(action: {
reset.newValue.wrappedValue = true
}, label: {
Text("reset tab view")
})
}
}
}
This resulted in awkward dismissal for views:
What i do for this is i make all my presentation bindings be stored using #SceneStorage("key") (instead of #State) this way they not only respect state restoration ! but you can also access them throughout your app easily by using the same key. This post gives a good example of how this enables the switching from Tab to Sidebar view on iPad.
I used this in my apps so if i have a button or something that needs to unwind many presentations it can read on all of these values and reset them back to wanted value without needing to pass around a load of bindings.

SwiftUI - Conditional based on ObservedObject doesn't work in subsequent Views

I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}

SwiftUI - Presenting a View on top of the current View from AppDelegate

I am working on a project that at some point it receives a notification. When that happens, I need to show a View. I am not able to catch notification from any View so I am looking for a way to change to control it from outside of View structs. After the View's purpose is done, I need to dismiss it where the app left off. Think like the native behaviour when there is an active call.
I thought I could use sheet however I could not find any way to trigger it for every View that could be active when the notifications come. Or maybe trying to extend native View class would work but again, no luck finding a tutorial.
Any help will be appreciated.
Just update your model based on notification. There is not necessary to define .sheet (modal view) everywhere in your view hierarchy. Doing it in root view should be enough.
To demonstrate that (copy - paste - run) I create small project where I mimic notification with SwiftUI Toggle.
import SwiftUI
class Model: ObservableObject {
#Published var show = false
}
struct SubView: View {
#EnvironmentObject var model: Model
var tag: Int
var body: some View {
VStack {
NavigationLink(destination: SubView(tag: tag + 1).environmentObject(model)) {
Text("subview \(tag)")
}
if tag == 2 {
Toggle(isOn: $model.show) {
Text("toggle")
}.padding()
}
}.navigationBarTitle("subview \(tag)")
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
SubView(tag: 0).environmentObject(model)
}.sheet(isPresented: $model.show) {
Text("sheet")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the result

#EnvironmentObject in SwiftUI - can't toggle bool

I have the class below to keep the state of the hamburger menu if it is or not displayed
class Menu: ObservableObject {
#Published var isActive: Bool = false
}
I instantiate it in Scene Delegate as such
let contentView = ContentView().environmentObject(Menu())
Then in a simple view i am trying to toggle the isActive bool, however i get the error below
struct Button: View {
#EnvironmentObject var menuState: Menu
var body: some View {
VStack{
Button(action: {
self.menuState.isActive.toggle()
}) {
Text("A")
}
}
}
}
This is the error i get: Cannot invoke initializer for type 'Button' with an argument list of type '(action: #escaping () -> (), #escaping () -> Text)'
The issue is that you named your custom View as Button, which is also the name of the existing SwiftUI button.
Simply rename your struct to something else and your code will compile just fine.
Unrelated to your question, but there's no point in wrapping a single View in a VStack, your body can simply contain the Button.
struct MyButton: View {
#EnvironmentObject var menuState: Menu
var body: some View {
Button(action: {
self.menuState.isActive.toggle()
}) {
Text("A")
}
}
}
You created a custom view named Button which is in conflict with the native SwiftUI Button (because they have the same name).
Rename your Button view and it will be okey I think.