Detecting tap gesture on the background of a GeometryReader SwiftUI - ios

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.

Related

Ignore SafeArea inside NavigationView

I am creating simple app with swiftui. In my app, I have 2 screens "ContentView" and "Home".
In my Content View,
struct ContentView : View {
var body: some View {
NavigationView {
ZStack {
//Some views
NavigationLink(isActive: self.$routeHome, destination: {Home},label: {
Text("Home")})
.navigationBarTitle("title")
}
.navigationBarTitle("Page1")
}
}
}
and In Home,
struct Home : View {
var body: some View {
ZStack {
Color.red.edgesIgnoringSafeArea(.all)
ScrollView{
//some views
}
}.navigationBarTitle("Home")
}
}
the Red color takes entire screen at first. but when I scroll, the NavigationView area is not in red color anymore. In my actual code, I want to show red color for all spaces when I click on button inside scrollview. Please help me to accomplish this in SwiftUI.
NavigationBar is colored by top child scrollView's offset.
If you want to always hide navigationBar background, you can use toolbarBackground() like following code.
struct Home : View {
var body: some View {
...
.navigationBarTitle("Home")
.toolbarBackground(.hidden, for: .navigationBar) // Added
}
}
P.S. I tried this codes with iPhone simulator 16.2
To ignore the safe area inside a NavigationView in SwiftUI, you can use the edgesIgnoringSafeArea(_:) modifier and pass it the .top and/or .bottom edges, like this:
NavigationView {
VStack {
// your content here
}
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.bottom)
}
This will allow the content of the VStack to extend under the top and bottom safe areas of the NavigationView. Note that this may cause the content to overlap with the system status bar and the navigation bar, so you may need to adjust the layout or add additional padding or spacing as needed.
Alternatively, you can use the .navigationBarHidden(true) modifier to hide the navigation bar entirely and allow the content to extend to the edges of the screen.
NavigationView {
VStack {
// your content here
}
.navigationBarHidden(true)
}

Draw a view over the navigation bar

I'm working on a bottom sheet that can be invoked from any other screen. The bottom sheet will be displayed on top of a half-opaque overlay and I would like the overlay to render full screen over any other view including the navigation bar and the tab bar.
However, I can't seem to be able to figure out how to get the content of the navigation bar to be behind the overlay. Here is what a demo of my current implementation looks like. As you can see, it's possible to interact with the content of the navigation bar even though it is visually displayed behind the overlay.
Half Screen
Full Screen
Back button is still active
And here is the simplified code of my current implementation:
import SwiftUI
struct MainNavigationView: View {
var body: some View {
NavigationView {
NavigationLink(destination: AnoterView()) {
Text("Navigate to the next screen")
}
}
}
}
struct AnoterView: View {
var body: some View {
ZStack {
Color(uiColor: .red)
.edgesIgnoringSafeArea(.all)
.navigationTitle("Test")
.navigationBarTitleDisplayMode(.inline)
ViewWithOverlay()
}
}
}
struct ViewWithOverlay: View {
var body: some View {
ZStack {
// I'd like this overlay to be rendered over the navigation bar
Color(uiColor: .blue)
.edgesIgnoringSafeArea(.all)
Color(uiColor: .green)
}
}
}
And the outcome:
As you can see, while the blue color, which represent my overlay, is drawn over the red color, the title and the back button are still displayed on top of the blue color.
I understand why this is happening, but I cannot think of any workaround in SwiftUI to fix this that can be invoked from any view.
Any help is appreciated.
If you want to overlay everything then it should be on root, including over NavigationView as well, ie.
ZStack {
NavigationView {
Color(uiColor: .red).edgesIgnoringSafeArea(.all)
}
ViewWithOverlay() // << here !!
}
.edgesIgnoringSafeArea(.all)
One thing you can do is to put the NavigationView inside a ZStack. This way it will be in a lower layer hidden by the layer above. Here is the code that completely hides the NavigationBar on the tap of the button.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
ZStack {
NavigationView {
Text("Hello World")
.navigationTitle("Welcome")
}
VStack {
}.frame(maxWidth: isPresented ? .infinity: 0, maxHeight: isPresented ? .infinity: 0)
.background(.green)
Button("Animate") {
withAnimation {
isPresented.toggle()
}
}
}
}
}

Keyboard not dismissing in SwiftUI with onTapGesture

I am trying to workout how to dismiss a keyboard in SwiftUI when the user taps outside a TextField. I followed some other posts that suggested using onTapGesture on a VStack and extending the View protocol to have a method to dismiss the keyboard that is called from the onTapGesture.
This is my content view:
struct ContentView: View {
#State var text = ""
var body: some View {
VStack{
Spacer()
TextField("Text", text: $text)
Text("\(text)")
Spacer()
}
.onTapGesture {
dismissKeyboard()
}
}
}
and I have extended the View protocol with:
extension View {
func dismissKeyboard() {
print("dismissing keyboard")
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
When I run the app, the keyboard is only dismissed when I tap in the area of the Text(). I would understand why this occurs if the Spacers are not there as the VStack only fills the area required by the views that it contains but I expected that the spacers would allow the onTapGesture to work for the whole screen as the VStack is pushed to the top and bottom of the screen but it still only works when tapping the Text().
Is there a reason why the onTapGesture doesn't work when I tap where the Spacers are and only works where the Text is?
You need to set a non-empty background like this:
ZStack {
Color.white.opacity(0.0000001)
VStack {
TextField("Text", text: $text)
Text("\(text)")
}
}
.onTapGesture {
dismissKeyboard()
}
SwiftUI always ignores gestures on top of things that it considers empty-space.
e.g. Color.white.opacity(0) will be considered empty-space as well and the tap gesture wont work if you do that.

SwiftUI: Button in NavigationBar won't fire after modal dismissal

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.

Placing interactable views in front of a NavigationView's hidden navigation bar

I have a NavigationView with a NavigationButton inside of it, but I cannot get the NavigationButton to be at the top of the screen and still be able to be pressed, even though the navigation bar is hidden.
This code:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
}
.navigationBarHidden(true)
}
}
Looks like , but I want it to look like .
I've tried adding a negative padding to the top of the VStack (with .padding([.top], -95), and it visually works, but then I can't interact with the button by tapping it (I think it is behind the hidden navigation bar). I've tried setting the VStack's zIndex to 10000 to solve that, but it still didn't work. Is there a way for me to move the button up to the top while still making sure that the button recognizes when it is being tapped?
Add a navigationBarTitle before hiding your navigation bar:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationButton(destination: Text("Button Clicked")) {
Text("Hello World")
.background(Color.yellow)
}
Spacer()
}
.navigationBarTitle(Text("Title")) // Add this line
.navigationBarHidden(true)
}
}
Add this modifier to your NavigationView edgesIgnoringSafeArea(.top).

Resources