I found a strange and frustrating behavior in SwiftUI. If you present a sheet from within a view in a navigation link and then (quickly and in order):
swipe down to dismiss the sheet
swipe back (from left edge to the right) to dismiss the navigation view
The app will freeze before going back to the root view. Backgrounding the app and foregrounding it will unstick it.
You have to perform the steps very quickly.
Is there a workaround? I would like both gestures to work without interfering with each other.
This code will reproduce the issue:
struct ContentView: View {
#State var showSheet = false
var body: some View {
NavigationView {
VStack {
NavigationLink("blah") {
VStack {
Button("Show Sheet") {
showSheet.toggle()
}
.sheet(isPresented: $showSheet, onDismiss: nil) {
Text("Sheet")
}
}
}
}
}
}
}
Here is a gif of the issue in the simulator. First showing the issue, then showing that if you background the app, it resolves on it's own.
EDIT: Oct 2022: This bug is still present in iOS 16
Related
I am trying to present a view as bottom sheet but it is behaving weirdly while closing the view using drag down. Whenever the keyboard is active it crops the view while dragging down but when keyboard is not active it behaves perfectly. I want to stop this cropping view when dropping down. You can more under stand in the GIFs.
When keyboard is not active [This what I want achieve when keyboard is active]:
When keyboard is active [Focus on edges of sheet] :
I have tried changing method of presenting but using SwiftUIX and iOS 16 sheet modifier. But I have not found the cause of this. And I am not getting any idea why this is happening and yes this behaviour only reproduces in iOS 16.
struct ContentView: View {
#State var presented: Bool = false
var body: some View {
Button("Show",action: {
presented.toggle()
})
.ignoresSafeArea()
.sheet(isPresented: $presented) {
view2
}
}
private var view2: some View {
VStack(spacing: 0) {
TextField(text: .constant("123"))
.frame(height: 70)
.background(.gray)
.padding()
TextField(text: .constant("456"))
.frame(height: 70)
.background(.gray)
.padding()
Spacer()
}
.ignoresSafeArea()
.background(.black)
}
}
I don't know why this issue is happening, but I have solved this issue by changing presenting approach.
First reason that making cropping issues is ignoring the safe area using any method will reproduce the same issue. So, you have to remove ignoreSafeArea() or edgesIgnoringSafeArea(). It will solve your problem but there's a chance you have to redesign your screen.
If it will still not work, try presenting the view using ViewController's present method by Creating an object of UIHostingController() by passing your view in it and presenting that UIHostingConrtoller() object.
AdaptToKeyboard() solution in the question comment works but not in every scenario. I had three points that had the same issue adaptsTokeyboard()solved the issue in two points but not o the third point.
Here's example of UIHostingController() approach
extension UIApplication {
public var firstKeyWindow: UIWindow? {
windows.first(where: { $0.isKeyWindow })
}
#available(macCatalystApplicationExtension, unavailable)
#available(iOSApplicationExtension, unavailable)
#available(tvOSApplicationExtension, unavailable)
public var topmostViewController: UIViewController? {
UIApplication.shared.firstKeyWindow?.rootViewController?.topmostViewController
}
func present<V: View>(_ view: V) {
previousTopmostViewController = UIApplication.shared.topmostViewController
let controller = UIHostingController(rootView: view)
previousTopmostViewController?.present(controller, animated: true)
}
}
in my ContentView, I have something analogous to this:
import SwiftUI
struct MainContentView : View {
var body: some View {
Text("Main Content View")
}
}
struct AlternateView : View {
var body: some View {
Text("Alternate View")
}
}
struct ContentView : View {
#State private var selection: String? = "Main"
var body: some View {
ZStack {
NavigationView {
VStack {
ZStack {
Color.clear
AlternateView()
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.top)
//button to go back to the MainContentView()
//AlternateViewBackButton(button_action: self.hide_alternate_view)
}
NavigationLink(destination:
MainContentView()
//why cant I do this without breaking NavigationView?
//.navigationBarBackButtonHidden(true)
,
tag: "Main",
selection: $selection)
{
EmptyView()
}
.navigationBarHidden(true)
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
My question is in the code commented above. Why does adding navigationBarBackButtonHidden(true) break the functionality where I can drag from the left of the screen to the right to animate between the two views? Is this something I am doing wrong? or is it a SwiftUI bug?
I encountered a solution which I thought would fix this problem, namely: Hide navigation bar without losing swipe back gesture in SwiftUI It works on the test case I have written above, but fails on the full program unless I deactivate all of the gestures I have installed on MainContentView with .simultaneousGesture
This partial solution may be included over the original code example with the following snippet (copied from the linked post)
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
It seems to work on the test example, but when I use it in my main project, it interferes with the gestures I have installed on MainContentView. I will try to figure out exactly why this is happening and include what I find below.
-edit-
The way this problem manifests in my codebase is as follows:
If I include the extension code snippet above, uncomment .navigationBarBackButtonHidden(true), and comment all gestures on MainContentView (including every one of its children), There is no back button (as intended) and I can drag from left to right to access the AlternateView (as intended). However, commenting the gestures on MainContentView and children is not possible, as those govern the core functionality of the application.
If I knew why this was happening, I would not have a question anymore.
It seems to me that .navigationBarBackButtonHidden should not change anything other than the fact that the back button is hidden.
-end of edit-
I need to be able to handle gestures on both MainContentView and AlternateView. Handling user input is a critical part of the program, after all.
One correlated question is this: why does the above code hide the navigationBar in portrait mode, but show it in landscape?
Thanks in advance for any help!
When dismissing a .sheet view in SwiftUI the view behind it is unresponsive for a second.
During my investigation of this bug I found that the Presentation Controller of the sheet is backed by a UITransitionView, which doesn't dismiss as fast as the sheet itself and is therefore blocking the taps. The problem is that this UITransitionView isn't a accessible in SwiftUI and this makes configuring the transition impossible in a way where I could prevent UITransitionView from blocking the view hierarchy.
Here is a minimal example on how to reproduce this behavior:
struct ContentView: View {
#State var showSheet: Bool = false
var body: some View {
VStack {
Text("Show the sheeet").onTapGesture(perform: {
self.showSheet.toggle()
})
}
.sheet(isPresented: self.$showSheet, content: {
VStack {
Button("Dismiss Sheet", action: {
self.showSheet.toggle()
})
}
})
}
}
After dismissing the sheet the view isn't responsive for ~1 second.
So is there a way to prevent this or do I have to right my own custom sheet?
I have a main view, and, within that view I have a little pop-up menu that is within a GeometryReader, like this:
if (self.show){
GeometryReader{_ in
Menu()
}.background(Color.black.opacity(0.65))
}
the line ~~~.background(Color.black.opacity(0.65))~~~ is essentially making the background (i.e. every part of the view that isn't in the pop up view, a bit dark. I would like to do something like this:
if (self.show){
GeometryReader{_ in
Menu()
}.background(Color.black.opacity(0.65))
.background(.onTapGesture{
print("asdf")
})
}
but this syntax isn't supported. Is there any way I can accomplish this? Essentially, I want it so that when I click outside of the GeometryReader, I can toggle a variable (in this case, get rid of the pop up view).
I tried just making a TapGesture Recognizer on the main view, but since the GeometryReader is part of the main view, when I tap on the GeometryReader pop up view itself, then it disappears.
Is there any way to accomplish something similar to the code I wrote above?
Thanks
Here is an example. I use three tapGestures:
one on the Main View to toggle the "Menu"
one on the "Menu" (do something there)
and one on the Background View to dismiss the "Menu" again,
like so:
struct ContentView: View {
#State private var showMenu: Bool = false
var body: some View {
ZStack {
// The Main View.
Text("Tap Me!")
.padding()
.onTapGesture {
showMenu.toggle()
print("Tapped Main View")
}
// The Menu View (shown on top of the Main View).
if showMenu {
GeometryReader { _ in
Text("Menu")
.padding()
.onTapGesture {
// do something here
print("Tapped Menu")
}
}
// The Background View that darkens the whole screen.
.background(
Color.gray.opacity(0.2)
.edgesIgnoringSafeArea(.all)
)
.onTapGesture {
showMenu.toggle()
print("Tapped Background")
}
}
}
}
}
A tap on the "Tap Me!" (main view) brings up the menu view. The "Menu" captures taps to act upon - do whatever you want to do there.
Whenever the user taps outside of the "Menu" the tapGesture on the background recognizes the tap and dismisses the "Menu" including the darkening background --> the main view lightens again.
I'm running into some weird behavior, trying to get a simple modal to pop after it has been dismissed.
I have an Add button in the NavigationBar that pops a modal. The modal has a button that will dismiss it, which works. However, I cannot interact with the Add button in the NavigationBar again until I interact with something else on the screen, such as scrolling the List below.
I have also placed another Add button, just for kicks, in the List itself, which always works.
Here's the code for the main view:
import SwiftUI
struct ContentView: View {
#State var displayModal: Bool = false
var body: some View {
NavigationView {
List {
Text("Hello again.")
Button(action: { self.displayModal = true }) {
Text("Add")
}
}
.sheet(isPresented: $displayModal) {
Modal(isPresented: self.$displayModal)
}
.navigationBarTitle("The Title")
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add")
})
}
}
}
And the modal, for completeness:
import SwiftUI
struct Modal: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Button(action: {
self.isPresented = false
}) {
Text("Cancel")
}
.padding()
Spacer()
}
Text("I am the modal")
Spacer()
}
}
}
The only thing I can think of is that something invisible is preventing me from working with the NavigationBar button. So I fired up the UI Debugger, and here's what the ContentView looks like. Note the NavigationBar button.
Now, after I tap the button and display the modal, and then use the UI Debugger to see the ContentView again, all the same elements are in place, but the Button parent views are offset a bit, like this:
Once I drag the List up and down, the UI Debugger shows a view hierarchy identical to the first image.
Does anyone have any idea what's going on here?
I'm using Xcode 11.2.1 and iOS 13 on an iPhone 11 Pro simulator, but have also observed this on my iPhone.
It is really a bug. The interesting thing is that after 'drag to dismiss' the issue is not observed, so it is a kind of 'sync/async' state changing or something.
Workaround (temporary of course, decreases visibility almost completely)
.navigationBarItems(trailing: Button(action: { self.displayModal = true }) {
Text("Add").padding([.leading, .vertical], 4)
})
I ran into the same issue, and for me the workaround was to use an inline-style navigation bar title on the presenter.
.navigationBarTitle(Text("The Title"), displayMode: .inline)
HOWEVER, if you use a custom accent color on your ContentView (like .accentColor(Color.green)), this workaround no longer works.
Edit: the bug seems fixed in 13.4, and no workarounds are needed anymore.