I have referred to this Stack Overflow thread SwiftUI How to Pop to root view.
However it doesn't work for my case, of course no answer fits each persons use case perfectly so one always has to modify the answer slightly but of course keeping to the overall outline of the solution.
I have looked at this thread How to popup multiple view off navigation stack, but I am not sure resetting the scene is the best option? There has got to be a "normal" way?
The solution I went with is use an ObservableObject and set it as an EnvironmentObject of the root view.
The navigation stack grows by 4 views:
RootView().environmentObject(AppSettings.shared)
FirstView()
SecondView()
ThirdView()
FourthView()
The NavigationLink isActive state for the FirstView is defined in AppSettings.shared, all the other states are found in the subsequent view and not in AppSettings.
For example:
FirstView -> SecondView the SecondView isActive state is defined in the ViewModel of the FirstView, and so on and so forth.
What I am trying to achieve is to pop to RootView from the FourthView. So on the FourthView there is an environmentObject variable of type AppSettings (passed down as an EnvironmentObject from RootView) and on button press, toggle RootView -> FirstView isActive state to false.
It toggles but there's no navigation.
However, in the debug console this is the error
Trying to pop to a missing destination at /Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-42.24.100/Shared/NavigationBridge_PhoneTV.swift:205
From my understanding toggling that state to false should trigger a navigation back, but in the Stack Overflow thread there was a post to use #State variable from RootView -> FirstView and in the EnvironmentObject have a variable moveToDashbord. Then on the RootView add a .onReceive modifier to listen to moveToDashboard publisher and then trigger #State variable. But again that also results in the same debug console message.
In short all solutions result in a missing destination console message. Is this because the navigation is too deep?
This is an iPad only project, and the navigationView style is set to StackedNavigationStyle.
System Details:
Xcode 11.6
iOS/ PadOS Target 13.0 (so not using SwiftUI 2.0, if that is a thing)
Code examples:
This is the SceneDelegate which sets the AppSettings as system wide EnvironmentObject.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LoginView().environmentObject(AppSettings.shared)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
This is an example of the navigationLink to the RootView
NavigationLink(destination: RootView().navigationBarBackButtonHidden(true), isActive: self.$loginViewModel.loginSuccess) {
EmptyView()
}
This is an example of navigationLink from RootView -> FirstView:
NavigationLink(destination: FirstView().environmentObject(FirstViewModel()), isActive: self.$appSettings.firstView) {
EmptyView()
}.isDetailLink(false)
Note. I had to change the actual names of the Views for clarification sake.
I found the issue, on one of the views there was a UIViewControllerRepresentable wrapped UIAlertController. It was structured as a view modifier with provision for content.
This basically wrapped over the original view which ended up breaking the navigation stack.
The solution was to rather wrap a UIViewController which is presented by the View and in that ViewController resides the UIAlertController.
This solution for the UIAlertController was used and adapted SwiftUI - UIAlertController
Related
I have a UIViewRepresentable that represents a PKCanvasView.
struct PKCanvasRepresentable : UIViewRepresentable
{
#Binding var canvas: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvas.tool = PKInkingTool(.pen, color: .black, width: 2)
canvas.drawingPolicy = .anyInput
canvas.isOpaque = false
canvas.backgroundColor = .clear
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {}
}
I want to use it as part of a sheet, that contains other input components and must be vertically scrollable.
#State var canvas = PKCanvasView()
var body: some View {
NavigationView {
ScrollView {
VStack {
// ..various components..
PKCanvasRepresentable(canvas: $canvas)
}
}
}
}
The drawing does not work, because the drawing gesture gets canceled by the scroll gesture.
I would like the PKCanvasView related gestures having priority over the scroll view ones. How can i achieve this?
Example
The expected behaviour can be seen when - for example - a DatePicker in wheel style is in a ScrollView. The scroll view does not receive any gesture input when interacting with the DatePicker. I would like to have the same behaviour for a PKCanvasView.
Additional info
I tried to add various Gesture modifiers to the Representable which prevents the ScrollView from getting input events - but of course that also prevents the Canvas from getting user input.
I built a drawing component myself in the past which worked, because i had control over the Gestures that were added to make the component happen. But i would prefer to use PKCanvasView, which does everything i need already - except from the described issue.
I saw this question - but it has nothing to do with PKCanvasView and its solution does not help.
I tried ai based code generators - but i don't have any subscriptions so i'm limited in tries and length of the answer. I tried the following quote, which produced only invalid, tutorial level answers:
Write a SwitUI view that contains of a ScrollView which has a nested UIViewRepresentable of a PKCanvasView where the ScrollView does not receive any kind of user input, events or gestures while the user interacts with the PKCanvasView and tries to draw but functions normally whenever the user does not interact with the PKCanvasView
My swiftui application structure looks like this
Navigation View (enclosing the landing view that is a list view )
On selection of a List item Navigation link directs to a Tab View with three tabs (default first tab)
When I use a sole standalone navigation link inside tab view screens to direct to another screen programatically, it navigates succesfully to the mentioned destination, but my binding doesn't work to come back to the previous screen.
Parent View
#State var showCameraPreviewView : Bool = false
ZStack{
Button("Show camera") {
showCameraPreviewView = true
}
NavigationLink(destination: CameraView(showCameraPreviewView: $showCameraPreviewView),isActive: $showCameraPreviewView){
EmptyView()
}
}
Child View
#Binding var showCameraPreviewView
Button("Assume capture success"){
self.showCameraPreviewView = false
}
Toggling showCameraPreviewView binding to false in the destination doesn't get me back to the current screen.
Looks straight forward, but doesn't work ! anything that I'm doing wrong ?
I can reproduce your issue, quite strange ... seems like the change of showCameraPreviewView is not accepted because the view is still visible. But I found a workaround with dismiss:
EDIT for iOS 14:
struct ChildView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var show: Bool
var body: some View {
Button("Assume capture success"){
show = false
presentationMode.wrappedValue.dismiss()
}
}
}
Is there anyway to keep the tab bar showing while presenting a modal / sheet view?
Here is a minimal failing example.
import SwiftUI
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
Button("Press to dismiss") {
dismiss()
}
.padding()
}
}
struct Tab1: View {
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
}
struct MainView: View {
var body: some View {
TabView {
Tab1()
.tabItem {
Label("Tab 1", systemImage: "heart")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
Thanks for answering my question in the comments.
Unfortunately the standard means of presenting views in SwiftUI is that they are truly modal – they capture the whole interaction context for the current scene, and you can’t interact with anything else until the modal is dismissed.
This is also the case for iPadOS. Even though a modal presented with .sheet on an iPad allows much more of the underlying view to be visible, you can’t interact with it until the sheet disappears. You can interact with different parts of the app by running two scenes side-by-side in split screen mode, but each half is a separate scene and any presented sheets are modal for that scene.
If you want one tab to optionally present a view over its usual content but still allow access to the tab view and its other tabs, that’s not a modal context and SwiftUI’s built-in sheet won’t work. You will have to implement something yourself - but I think that’s doable.
Rather than using .sheet, you could optionally add an overlay to your Tab1 view, using the same boolean state variable showingSheet. In this approach, the default dismiss environment variable won’t be available, so passing in the state variable as a binding value would be an alternative:
var body: some View
<main display>
.overlay(showingSheet ? Sheet1(presented: $showingSheet) : EmptyView())
You might also find that a ZStack works better than .overlay depending on what the contents of the tab view actually are.
You’ll definitely have a lot more structural work to do to make this work, but I hope you can see that it’s possible.
So for my own reasons, I need the full control that UITextField and its delegates would normally offer, but the screen it's being added to is written with SwiftUI. Needs a .decimal keyboard, so no Return Key.
I have only 2 issues remaining, 1 of which I hope to resolve here. Attempting to add a call to resign first responder and add it to a VStack on the page basically disables the UITextField, since I can't bring up the keyboard.
How can I dismiss this keyboard without adding an arbitrary extra button to the page?
Example Code:
Struct CustomTextView: UIViewRepresentable {
/// Insert init, updateView, binding variable, coordinator, etc
func makeView() -> UITextField {
var textField = UITextField()
textField.delegate = context.coordinator
/// Set up rest of textfield parameters such as Font, etc.
return textField
}
}
extension CustomTextView {
class Coordinator: NSObject, UITextFieldDelegate {
/// UITextfield delegate implementations, extra reference to binding variable, etc
/// Primarily textField.shouldChangeCharactersInRange
}
}
struct ContentView: View {
#State viewModel: ViewModel
var body: some View {
VStack {
CustomTextView($viewModel.parameter)
/// Other views
}
.onTap {
/// Attempting to add the generic call to UIApplication for resignFirstResponder here does not allow CustomTextView to ever hold it even when tapped in
}
}
}
I can't give all the code for privacy reasons, but this should be enough to establish my issue.
I have done this by adding this Function to you view below.
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
Then with a ontapGesture you can make the keyboard go away.
For example you can use this on the background Stack of your whole view. If a user taps on the background the keyboard will dissapear.
.onTapGesture {
self.hideKeyboard()
}
So I found a trick on my own with an epiphany overnight.
First, I would like to share to anyone else a very basic reason why inb4cookies solution wasn't quite adequate. While I had already tried adding a resignFirstResponder call like it to the onTap of the background stack, it was triggering the onTap for the VStack when I was clicking the field.
This is likely because I am using a UITextField as the back end for this component and not a SwiftUI TextField.
However, it was partially used in the final solution. I still applied it, but there is an extra step.
VStack {
CustomTextView($viewModel.parameter)
.onTap {/*Insert literally any compiling code here*/ }
/// Other views
}
.onTap {
self.hideKeyboard()
}
You'll see that above, there is an extra onTap. I tested it with a print statement, but this will override the onTap for the VStack and prevent the keyboard from being dismissed right after it is brought up. Tapping anywhere else on the VStack still closes it, except for Buttons. But I can always add hideKeyboard to those buttons if needed.
I have a button in a view (inside a NavigationView) that opens a full screen cover - a loading screen while some data is processing. When the cover is dismissed, I want to automatically route to the next view programmatically. I'm using a NavigationLink with a tag and selection binding, and the binding value updates when the cover is dismissed, but the routing doesn't happen unless I tap that same "open modal" button again.
import SwiftUI
struct OpenerView: View {
#EnvironmentObject var viewModel: OpenerViewModel
#State private var selection: Int? = nil
#State private var presentLoadingScreen = false
var body: some View {
VStack {
NavigationLink(destination: SecondScreen(), tag: 1, selection: $selection) { EmptyView() }
Button(action: {
viewModel.frequency = 0
self.presentLoadingScreen.toggle()
}, label: {
Text("Open cover")
}).buttonStyle(PlainButtonStyle())
}
.navigationBarTitle("Nav title", displayMode: .inline)
.fullScreenCover(isPresented: $presentLoadingScreen, onDismiss: {
self.selection = 1
}, content: ModalView.init)
}
}
struct ModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Howdy")
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
presentationMode.wrappedValue.dismiss()
}
}
}
}
The first time I hit the Button, the cover opens. Inside the cover is just a DispatchQueue.main.asyncAfter which dismisses it after 2 seconds. When it's dismissed, the onDismiss fires, but then I have to hit the button again to route to SecondScreen.
Any ideas?
Edit: Added the modal's View
I got it working with some changes to the code, and I'm sharing here along with what I think is happening.
I believe the problem is a race condition using a #State boolean to toggle the cover and the navigation. When the cover is being dismissed, my main OpenerView is being recreated - to be expected with state changes. Because of this, I try to set the #State var selection to trigger the navigation change, but before it can do so, the view is recreated with selection = nil.
There seem to be two ways to solve it in my case:
Move the cover boolean to my view model - this worked, but I didn't want it there because it only applied to this view and it's a shared view model for this user flow. Plus, when the modal is dismissed, you see the current OpenerView for a brief flash and then get routed to the SecondScreen.
Keep the cover boolean in #State, but trigger the navigation change in the button immediately after setting the boolean to open the modal. This worked better for my use case because the modal opens, and when it closes, the user is already on the next screen.
I had a similar problem where I was trying to draw a view after dismissing a fullScreenCover. I kept getting an error that said that the view had been deallocated since it was trying to draw to the fullScreenCover.
I used Joe's hints above to make this work. Specifically:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewToShow()
}
I had previously tried onChange, onDisappear, onAppear - but none of those fit the use case I needed.