I'm trying to use NavigationLink inside List. For a specific reason, it is in .background with EmptyView().
var body: some View {
List {
Section {
HStack {
Image(systemName: "checkmark")
.frame(width: 30, height: 30, alignment: .center)
.opacity(selected ? 1 : 0)
Text("TEST")
Spacer()
}
.onTapGesture {
selected.toggle()
}
.background(
NavigationLink(destination: WifiView()) { EmptyView() }
.disabled(isPad)
)
}
}
}
The problem is the touch area for the NavigationLink is not as expected.
.background's area is as above.
but NavigationLink and EmptyView's area is as above. I tried to force the frame with .frame but it won't change.
What am I missing?
try something like this, works well for me:
var body: some View {
List {
Section {
HStack {
Image(systemName: "checkmark")
.frame(width: 30, height: 30, alignment: .center)
.opacity(selected ? 1 : 0)
Text("TEST")
Spacer()
}
.contentShape(Rectangle()) // <--- here
.onTapGesture {
selected.toggle()
}
.background(
NavigationLink(destination: WifiView()) { EmptyView() }
.disabled(isPad)
)
}
}
}
To distill the accepted answer: all you need to do is to add .contentShape(Rectangle()) to the same element which has .onTapGesture. This solution works for any container, not just List.
E.g. if you have:
VStack {
// ...
}
.onTapGesture {
// ...
}
Add contentShape to the same element:
VStack {
// ...
}
.contentShape(Rectangle())
.onTapGesture {
// ...
}
Rationale:
If you add a tap gesture to a container SwiftUI view, such as VStack or HStack, then SwiftUI only adds the gesture to the parts of the container that have something inside – large parts of the stack are likely to be untappable.
If this is what you want then the default behavior is fine. However, if you want to change the shape of hit tests – the area that responds to taps – then you should use the contentShape() modifier with the shape you want.
(source)
Related
I'm trying to implement a view that displays error message for my whole app.
I want this view to always be above every other view, but I also use sheets in my app and in that case the error message is hidden behind the sheet, since the sheet is displayed above every other view.
Here is a View to reproduce my situation:
struct AppView: View {
#State var isPresentingSheet = false
var body: some View {
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.sheet(isPresented: $isPresentingSheet) {
Text("Im above everything else")
}
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.2))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
}
}
I want to know if it's possible to display a view above a sheet, but to me it looks like the sheet is in a completely different window?
But maybe it's possible to create a custom sheet that moves in from the top and is displayed above other native sheets?
If anybody is interested, I have created a custom bottom sheet with simple controls and snap functionality.
The bottom sheet has a zIndex of 1 so you can easily place views above it with a greater zIndex.
You can find it here: AlternativeSheet.
Here is the code to achieve the view of the image above.
import AlternativeSheet
...
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.alternativeSheet(isPresented: $isPresentingSheet, snaps: [0.95]) {
Text("Im above everything else")
}
.isDraggable()
.dampenDrag()
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.3))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
Thanks to a previous question I asked, I am using Color.black.overlay and .clipped() to show an image with letterbox borders above and below it.
But when I attempt to put a button on the top border, it can't be tapped on (I assume because the image (unclipped) is in that space, and is intercepting the tap gesture).
Here is what the layout looks like:
Here is the code:
var body: some View {
ZStack {
Color.black
VStack {
topBorder
imageMiddle
bottomBorder
}
}
.ignoresSafeArea()
}
var topBorder: some View {
return Group {
ZStack {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
Button {
print("tap")
} label: {
Image(systemName: "hand.tap.fill")
.foregroundColor(.black)
}
}
}
}
var bottomBorder: some View {
return Group {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
}
}
var imageMiddle: some View {
return Group {
Color.black.overlay(
Image("cat")
.scaledToFill()
)
.clipped()
}
}
How can I expose that button to a user's tap?
Adding .allowsHitTesting(false) to your image view will fix it. However, it seems like the wrong approach.
VStack {
topBorder
imageMiddle
.allowsHitTesting(false) // <- This will fix your problem.
bottomBorder
}
I would recommend using another approach to add your borders on top of the image instead. Something like this:
ZStack {
imageMiddle
VStack {
topBorder
.overlay(alignment: .bottom) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
Spacer()
bottomBorder
.overlay(alignment: .top) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
}
}
.ignoresSafeArea()
Before Slide
After Slide
struct SlideView: View {
#State var isClicked = false
var body: some View {
HStack{
Rectangle()
.fill(.gray)
.ignoresSafeArea()
.frame(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0)
VStack{
Text("This gets squished")
HStack{
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
}
}
}
So I have this view that kind of "slides in" and it's pretty close to what I want but not exactly, the view that slides in ends up squishing the other view and compressing it to be about 25% of it's size. Of course this makes sense because it's all in a HStack and I end up taking 75% of the HStack's space but I was wondering if anybody had an idea of how I could do something like this but without squishing the second view that gets pushed away? So basically keep it the same size and just have the first 1/4 of the view visible and the other 3/4 just be gone I guess.
The problem is the view that gets squished has no explicit width. Understand how views get sized in SwiftUI. The parent view proposes a size, and the child view responds with the size it wants. Before you click the button, the parent view (the HStack) is proposing 100% to the view. That view says that I can do 100%, so no squishing. However, when you make the other view 75% of the parent, the parent can only offer 25% to the first view. The view responds, I can fit if I squish, so it does. See Laying out a simple view.
To fix it, you simply need to explicitly set the width of the first view to 100%, and then it will get pushed over like you were expecting without compression.
struct SlideInView: View {
#State var isClicked = false
var body: some View {
// I swapped out UIScreen.main.bounds for a GeometryReader
// UIScreen.main.bounds.width always returns the width of the screen
// even if the parent view does not have that much space to offer, and
// can lead to some interesting results.
GeometryReader { geometry in
HStack{
Rectangle()
.fill(.gray)
.ignoresSafeArea()
.frame(width: isClicked ? geometry.size.width * 0.75 : 0)
VStack{
Text("This gets squished")
HStack{
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
.frame(width: geometry.size.width) // Explicitly set width here
}
}
}
}
Personally, I think it would make more sense to just use .offset, and put them in a ZStack:
ZStack {
VStack {
HStack {
Spacer()
Text("yellow")
Spacer()
}
Spacer()
}
.background(.yellow)
VStack {
Text("red")
HStack {
Button{
withAnimation(.spring()) {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
.background(.red)
.offset(.init(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0, height: 0))
}
And you can add another .offset modifier to the yellow one to make it looks slidein:
VStack {
//...
}
.background(.yellow)
.offset(.init(width: isClicked ? 0 : -UIScreen.main.bounds.width * 0.75, height: 0))
What is an effective & effecient way to get the Dismiss button (X) into the top right corner?
I'm struggling with container alignment... can't say I GROK it.
Needless to say ... this ain't it!
var body: some View {
ZStack {
Image("Biz-card_2020")
.resizable()
.edgesIgnoringSafeArea(.all)
HStack(alignment: .top) {
VStack() {
Spacer(minLength: 5) // vertical space
HStack() {
Spacer()
// close Welcome page (X) button
Button(action: {
//print(" - Button to dismiss page \(self.isPresented)")
self.isPresented = false // dismiss the Welcome view
//print(" - after action Button to dismiss Welcome page \(self.isPresented)")
}, label: {
Image(systemName: "xmark.circle" )
.scaledFont(name: "Georgia", size: Const.titleText)
.minimumScaleFactor(0.3)
.accentColor(.white)
.padding(10)
})
}
Spacer()
}
}
}
}
You need to remove Spacer(minLength: 5) and replace it with padding for HStack.
Spacer(minLength: 5) doesn't mean its length will be exactly 5 (only that the minimum length will be 5).
You may also want to extract close button to another function for clarity.
Try the following:
struct ContentView: View {
...
var body: some View {
ZStack {
Image("Biz-card_2020")
.resizable()
.edgesIgnoringSafeArea(.all)
closeButton
}
}
var closeButton: some View {
VStack {
HStack {
Spacer()
Button(action: {
...
}) {
Image(systemName: "xmark.circle")
.padding(10)
}
}
.padding(.top, 5)
Spacer()
}
}
}
ZStack can be configured using .topTrailing etc. You should use those alignments to configure your view. Using something like Spacer() is going to cause the rest of your views to get pushed down.
ZStack(alignment: .topTrailing) {
// your code here
}
If you need to move your code a bit more, use either padding, or offset modifiers.
I hope to make a list just like to-do list, the cell has a checkbox and Text. And I want when list cell selected, the view could be translate to detailView. Meanwhile I also could select the checkbox, but don't trigger the view translate to the detailView.
I removed the List{}, the code run ok, but the List property and method can't use any more. So I plan to code a customer list view. But I don't think it will be the good method. So if I still can't find a good way to make it, I may implement in this way at last.
NavigationView {
VStack {
List {
ForEach(todayDietModel.todayDietItems) { item in
NavigationLink(destination: DietItemDetailView()) {
TodayDietRow(todayDietItem: item)
.animation(.spring())
}
}
}
}
.frame(width: 352, height: 400)
.background(Color.white)
.cornerRadius(16)
}
struct TodayDietRow: View {
#State var isFinished: Bool = false
var body: some View {
HStack(spacing: 20.0) {
Button(action: {self.isFinished.toggle()}) {
if self.isFinished {
Image("icon_checked_s001")
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
} else {
Image("icon_unchecked_s001")
.resizable()
.frame(width: 24, height: 24)
}
}
.padding(.leading, 10)
Text("DietName")
.font(.system(size:13))
.fontWeight(.medium)
.foregroundColor(Color.black)
}
.frame(width: 327, height: 48)
}
}
This works quite well with a gesture on the checkbox image. Two Buttons didn't work since every tap went to both buttons.
struct TodayTodoView2: View {
#ObservedObject var todayDietModel = TodayDietModel()
func image(for state: Bool) -> Image {
return state ? Image(systemName: "checkmark.circle") : Image(systemName: "circle")
}
var body: some View {
NavigationView {
VStack {
List {
ForEach($todayDietModel.todayDietItems) { (item: Binding<TodayDietItem>) in
HStack{
self.image(for: item.value.isFinished).onTapGesture {
item.value.isFinished.toggle()
}
NavigationLink(destination: DietItemDetailView2(item: item)) {
Text("DietNamee \(item.value.dietName)")
.font(.system(size:13))
.fontWeight(.medium)
.foregroundColor(Color.black)
}
}
}
}
.background(Color.white)
}
}
}
}