SwiftUI - Detect Tap Outside View or Stack - ios

Is there is a way to detect a tap outside View/HStack/VStack in SwiftUI?
I have tried to search stack overflow for a similar question, but all I could find are questions about how to dismiss the keyboard.
I have also tried to search the available HSTACK methods, and couldn't find any specific method handling click outside the view.

I believe the answer here will work for you: Detect touch outside Button in SwiftUI
struct ContentView: View {
var onConfirmPress: () -> Void
#State private var tappedOutsideView: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
// a transparent rectangle under everything
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.opacity(0.001) // <--- important
.layoutPriority(-1)
.onTapGesture {
self.tappedOutsideView = true
}
// your view
}
}
}
}

Related

SwiftUI - Adding dimmer view behind custom alert

I created a view modifier for reusable custom alert which works as expected. I now want to add a dimmer view in between presenting view and alert view i.e. view behind the alert which covers full screen and disables any clicks on the presenting view.
I tried adding background on the presenting view when alert is presented, but nothing is happening.
Custom alert view modifier, view extension and view model:
import Foundation
import SwiftUI
struct CustomAlertView: ViewModifier {
#Binding var isPresented: Bool
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}
func body(content: Content) -> some View {
content.overlay(alertContent())
}
#ViewBuilder
private func alertContent() -> some View {
GeometryReader { geometry in
if self.$isPresented.wrappedValue {
VStack {
Image(systemName: "info.circle").resizable().frame(width: 30.0, height: 30.0).padding(.top, 30).foregroundColor(.cyan)
Text("Error title").foregroundColor(Color.black).font(.title2).bold().lineLimit(nil).padding([.leading, .trailing], 24.0).padding(.top, 16.0)
Spacer()
Text("There was an error while processing your request.").foregroundColor(Color.black).font(.body).lineLimit(nil).padding([.leading, .trailing], 18.0).padding(.top, 16.0)
Spacer()
Button(action: { self.$isPresented.wrappedValue.toggle() }) {
Text("Ok").foregroundColor(.white).font(.largeTitle).bold()
}.padding(.bottom, 25.0)
}.fixedSize(horizontal: false, vertical: true)
.background(Color.purple)
.cornerRadius(10)
.clipped()
.padding([.leading, .trailing], 5.0)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.frame(width: 328.0)
}
}
}
}
extension View {
func customAlert(isPresented: Binding<Bool>) -> some View {
return modifier(CustomAlertView(isPresented: isPresented))
}
}
class CustomViewModel: ObservableObject {
#Published var showAlert = false
func doSomething() {
// Sets showAlert to true incase of network disconnect or some query failure.
self.showAlert = true
}
}
Content view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: CustomViewModel = CustomViewModel()
var body: some View {
VStack {
Spacer()
Button(action: { viewModel.doSomething() }) {
Text("Start").foregroundColor(Color.red).font(.title)
}.padding(.bottom, 100.0)
}
.background(viewModel.showAlert ? Color.gray : Color.clear)
.customAlert(isPresented: $viewModel.showAlert)
}
}
Here, ContentView is the presenting view since it's what is presenting the alert. I want to add a grayish sort of view/dimmer view covering full screen and it will be below the presented alert. When dimmer view is present and I click on "Start" button in ContentView, it should be disabled. I don't know if I can achieve this by modifying the custom alert view modifier, hence I was trying to add a background color to ContentView, but nothing seems to be happening. I have too much of code in the view model and content view, so I removed most of it and kept what I thought was needed.
How do I achieve it?
I was able to add a view behind the custom alert view modifier. Adding code here incase if someone comes looking for it in future.
I removed ".background(viewModel.showAlert ? Color.gray : Color.clear)" from my content view since I eventually wanted the logic to be part of custom alert view modifier and not add it to every view. In my customAlertView, I modified the body function as below:
func body(content: Content) -> some View {
content.overlay(self.$isPresented.wrappedValue ? Color.gray.ignoresSafeArea() : nil)
.overlay(self.$isPresented.wrappedValue ? alertContent() : nil)
}

SwiftUI: fade out view

I have the following code:
struct ContentView: View {
#State var show = false
var body: some View {
VStack {
ZStack {
Color.black
if show {
RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.transition(.opacity)
}
}
Button {
withAnimation(.easeInOut(duration: 1)) {
show.toggle()
}
} label: {
Text("TRIGGER")
}
}
}
}
I want the RoundedRectangle to fade in and out. Right now it only fades in. This is a simplified version of a more complex view setup I have. Depending on the state I may have the view I want to fade in or not. So, I am looking for a way to fade in (like it works now) but then also fade out so that the view is totally removed from the hierarchy and not just hidden or something.
How can I have this code also fade OUT the view and not only fade in?
As a reference I followed this approach:
https://swiftui-lab.com/advanced-transitions/
....
if show {
LabelView()
.animation(.easeInOut(duration: 1.0))
.transition(.opacity)
}
Spacer()
Button("Animate") {
self.show.toggle()
}.padding(20)
....
But, in my case it is NOT fading out.
SwiftUI ZStack transitions are finicky. You need to add a zIndex to make sure the hierarchy is preserved, enabling the animation.
RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.transition(.opacity)
.zIndex(1) /// here!
You need to link the opacity directly to the state, so that it is directly animating any changes.
struct ContentView: View {
#State var show = false
var body: some View {
VStack {
ZStack {
Color.black
(RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.opacity(show ? 1 : 0)
)
}
Button {
withAnimation(.easeInOut(duration: 1)) {
show.toggle()
}
} label: {
Text("TRIGGER")
}
}
}
}
EDIT: to reflect the comment requiring the view to be removed, not just faded out...
To remove the view (and trigger .onDisappear) you could modify as below:
ZStack {
Color.black
show ? (RoundedRectangle(cornerRadius: 20)
.fill(.brown)
.zIndex(1). //kudos to #aheze for this!
).onDisappear{print("gone")}
: nil
}
This will fade in/out as above, but will actually remove the view & print "gone"

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()
}
}
}
}
}

SwiftUI: SearchBar moving out of bound when the List is too long

I've implemented a search bar in my app inside a custom header. Beneath the search bar I have added a false List with 100 rows that is intended to show search results.
The problem that I'm facing is:
when the list appears, the search bar moves out of bounds. When I add a top padding of 400px, the search bar comes back to bounds. Link to video 1
The next two are a bit out of topic.
When the keyboard is on screen, the last few rows of the list are not visible. How to fix it? Link to video 2
How to set a background color for a List? I haven't been able to figure that out. (listRowBackground modifier isn't working as suggested by an article I read.)
I'm using Xcode 12.0 beta 6.
let screen = UIScreen.main.bounds
struct SearchBarView: View {
#Binding var search: String
#Binding var searchSelected: Bool
var body: some View {
VStack {
CustomTextField(text: $search, isFirstResponder: true)
.modifier(SearchBarTextFieldStyle(search: $search))
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: screen.width)
}
}
.frame(width: screen.width, height: !search.isEmpty ? screen.height : 40)
.background(Color("ThemeColor"))
}
}
when the list appears, the search bar moves out of bounds. When I add
a top padding of 400px, the search bar comes back to bounds.
The issue is that everything is placed in one VStack. So when search is not empty anymore the TextField shares the space provided to it with the List.
Place the TextField in a separate Stack like this:
struct ContentView: View {
#State var search: String = ""
var body: some View {
// Everything wrapped in one Stack
VStack() {
// Separate Stack for the TextField
HStack() {
TextField("Title", text: self.$search)
}.padding()
// One Stack for the content
VStack {
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: UIScreen.main.bounds.width)
}
}.frame(width: UIScreen.main.bounds.width)
.background(Color.red)
Spacer() // So that the TextField is also on top when no content is displayed
}
}
}
When the keyboard is on screen, the last few rows of the list are not
visible. How to fix it?
Add a padding to the bottom of the list but I'd recommend implementing the solution of this: Move TextField up when the keyboard has appeared in SwiftUI
E.g. with padding:
import SwiftUI
struct ContentView: View {
#State var search: String = ""
var body: some View {
VStack() {
HStack() {
TextField("Title", text: self.$search)
}.padding()
VStack {
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: UIScreen.main.bounds.width)
.padding(.bottom, 300) // here padding
}
}.frame(width: UIScreen.main.bounds.width)
Spacer()
}
}
}
How to set a background color for a List? I haven't been able to
figure that out. (listRowBackground modifier isn't working as
suggested by an article I read.)
This question has also already been answered here:
SwiftUI List color background

How to find out if view is currently selected in SwiftUI

I'd like to find out whether my view is currently selected or not.
I have a rectangle which represents a card. Once I tap the card, the card should go into the selected state and change properties like the color.
But once I tap somewhere else, the card should not be in that state anymore.
So far, I did not manage to figure this out as I tried with the onTapGesture property but with that I run into the problem that when I tap outside that card, the card does not change the state to false unless I tap the card again, which makes sense but it seems to be the wrong choice in this case.
import SwiftUI
struct CardView: View {
#State var selected = false
let rectangle = Rectangle()
#State var rectangleColor: Color = .purple
var body: some View {
GeometryReader { g in
self.rectangle
.foregroundColor(self.rectangleColor)
.frame(width: g.size.width / 2, height: g.size.width / 2)
.onTapGesture {
self.selected.toggle()
self.modifyColors()
}
}
}
func modifyColors() {
if selected {
rectangleColor = .red
} else {
rectangleColor = .purple
}
}
}
The selected state is the red color, the unselected one the purple color.
So I want that the color becomes red when I tap into the rectangle but not if I tap outside it.
It should become purple only again when I tap outside the rectangle but not inside it.
Example: Card is purple. I select it and it becomes red. When I then tap it again, it should stay red. It should become only purple (unselected) when I tap somewhere outside the card but not inside it.
View that contains this rectangle:
import SwiftUI
struct ContentView: View {
var body: some View {
CardView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It shouldn't matter where the view is actually - I want to know weather the space around that view is tapped. So if it is inside a modal, or the top view on a ZStack, the behavior should be the same.
The way to achieve this is by having the parent view manage both the selection and the gestures.
The CardView just takes a #Binding to isSelected and changes the colour accordingly:
struct CardView: View {
#Binding var isSelected: Bool
var body: some View {
Rectangle()
.fill(self.isSelected ? Color.red : Color.purple)
.aspectRatio(1.0, contentMode: .fit)
}
}
While the parent manages the #State and updates it using gestures.
struct ContentView: View {
#State var isCardSelected = false
var body: some View {
ZStack {
Color.white
.onTapGesture {
self.isCardSelected = false
}
CardView(isSelected: $isCardSelected)
.onTapGesture {
self.isCardSelected = true
}
}
}
}

Resources