SwiftUI: Increase tap/drag area for user interaction - swift

I've built a custom slider in SwiftUI with a thumb dragger that is 20x20. Everything is working, except the tap target is quite small since it's just the 20x20 view. I'm looking for a way to increase this to make it easier for users to interact with my slider. Is there a way to do this in SwiftUI?
I tried wrapping my thumb in Color.clear.overlay and setting the frame to 60x60. This works if the color is solid, like red, but with clear the tap target seems to revert back to visible pixels of 20x20.
You can see on this gif I'm able to drag the slider even when clicking outside of the thumb.
However, as soon as I change the color to clear, this area no longer receives interactions.

Add a .contentShape(Rectangle()) after the frame.

I'm sure there are several ways to accomplish this, but this is how i made a big button with the whole area tappable:
Button(action: { someAction() }, label: {
Text("OK")
.frame(minWidth: 200)
.contentShape(Rectangle())
.padding()
})
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(5.0)

I had the same problem. Except I didn't want the expanded overlay to impact the rest of the layout. If you embed everything in a ZStack and put a rectangle before your interactive object, you can control the size of the gesture. Kind of like this:
Rectangle().frame(width: screen.width, height: 300).opacity(0.001)
.layoutPriority(-1)
Just needed to make sure to set the opacity to next to nothing so you can't see it, and the layout priority to -1 so it doesn't impact the view.

As a follow up to the answer above, You can enlarge the clickable rect all day using this as example.
ZStack {
Image(systemName: secured ? "eye.slash" : "eye")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
Rectangle()
.frame(width: 22, height: 20)
.opacity(0.001)
.onTapGesture {
self.secured.toggle()
}
}

Related

SwiftUI: disable interactions on transparent parts of ScrollView

Assuming I have a horizontal ScrollView, where the first item has a view a lot taller than the rest. Is there any way to limit the interaction to the visible elements (black rectangles in the image below)?
ScrollView(.horizontal) {
HStack(alignment: .bottom) {
VStack(alignment: .center, spacing: 1) {
Rectangle()
.frame(width: 20, height: 70)
Rectangle()
.frame(width: 40, height: 40)
}
ForEach((1...10), id: \.self) { _ in
Rectangle()
.frame(width: 40, height: 40)
}
}
}
.background(.yellow)
I don't want to have the invisible area here blocking everything beneath that view, so everything in the red area should not scroll on the scrollview.
In my case, there’s a zoomable ScrollView in horizontal and vertical direction behind it and text all over it that can be tapped on (live text image view).
Here are my ideas, I was either not smart enough to properly implement them or it's simply not possible:
allowsHitTesting(false) on ScrollView and enable on the child views: child views can't be enabled if parent is off
.contentShape(...) on ScrollView: doesn't disable scrolling, also I found no way to manually draw the contentshape around the content
move the first item down and use .offset(y: -300) on the first item to move it back up: item is clipped outside the ScrollView
use UIViewRepresentable with UIScrollView and gestureRecognizerShouldBegin(...) to let all gestures on that area through: I don't know how to implement that (would this be possible?)
move the first item outside the ScrollView: in theory works, but not ideal since the item no longer scrolls and I couldn't get the animations of reordering right
detach the "upper" view, track the position of the item inside the ScrollView and move the upper view manually: probably would lag behind, also not a very elegant solution
Is one of them actually possible? Any other ideas? Thanks :)
How about something like this:
ZStack(alignment: .top) {
ScrollView(.horizontal) {
HStack(alignment: .bottom) {
Rectangle()
.frame(width: 20, height: 90)
ForEach((1...15), id: \.self) { _ in
Rectangle()
.frame(width: 20, height: 20)
}
}
}
.background(.yellow)
Text("test")
.frame(width: 100, height: 50)
.background(.red)
}
Text is in front of you ScrollView and can't be hit, thus not triggering your ScrollView. From your question, it's hard to guess, what content lies behind your ScrollView and if any further culprits may apply.

How to set the opacity of an overlay in SwiftUI

I'm trying to set the opacity of an overlay in SwiftUI.
However, I'm only trying to change the opacity of the overlay, and want to keep the opacity of the symbol that is being overlayed. But, when i apply the .opacity() modifier, it changes the opacity of the whole view (both the image and the overlay)
Image(systemName: "magnifyingglass")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.padding(10)
.overlay(Color.gray)
.opacity(0.1)
.clipShape(Circle())
this code produces this view.
as you can see, the entire view is barely visible, and I am trying to only make the gray circle around the search icon opaque, and keep the opacity of the search icon itself.
You're adding opacity to your whole view, add it to your overlay instead:
.overlay(Color.gray.opacity(0.1))
Why are you overlaying the gray circle on top of the symbol? Shouldn't it be the other way around? The order of view modifiers, including .opacity(), are very important in SwiftUI. If you want to only adjust the opacity of the gray circle, you must attach .opacity() before the overlay.
Color.gray
.frame(width: 40, height: 40)
.clipShape(Circle())
.opacity(0.1) /// apply to the gray circle
.overlay( /// overlay does not get affected
Image(systemName: "magnifyingglass")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(10)
)
Result:

SwiftUI content gets cut off on smaller iPhone screens

I'm new to SwiftUI, so I'm following a tutorial to get familiar with it. However, my app's content is getting cut off on smaller screens (both vertically and horizontally). How can I prevent this from happening?
Here's my code:
EDIT: I have added borders around my images and resized the images as suggested in your comments and answers, but as you can see, the images don't appear to be taking up any more space than they're supposed to.
struct ContentView: View {
var body: some View {
ZStack {
Image("background").ignoresSafeArea(.all)
VStack {
Spacer()
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
.border(Color.black)
Spacer()
HStack {
Spacer()
Image("card3").border(Color.black, width: 3)
Spacer()
Image("card4").border(Color.black, width: 3)
Spacer()
}
Spacer()
Image("dealbutton")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
.border(Color.black)
Spacer()
HStack {
Spacer()
VStack {
Text("Player").padding(.bottom, 10)
Text("0").font(.largeTitle)
}
Spacer()
VStack {
Text("CPU").padding(.bottom, 10)
Text("0").font(.largeTitle)
}
Spacer()
}
.foregroundColor(.white)
Spacer()
}
}
}
}
And here's what the preview looks like on an iPod touch:
Try making Image("background") resizable or set it as the .background(:) of your ZStack. Currently the background image isn’t resizable and is larger than the screen, so it shows at its native size and stretches its parent ZStack beyond the bounds of the screen. Since your content is in that same ZStack, it also extends beyond the bounds of the screen
Your issue is related to your images that you have present on the view structure itself. Images are rendered at 100% their size, irrespective of their constraints. This will cause other views to be pushed away. The solution for that is to set a set size on the view itself that matches within the confines of your available space. Also you're resizing your ZStack which also resizes the content inside of the ZStack. For example.
Image("logo")
.resizeable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Handling your image like this will ensure that it is set to the appropriate size when it is rendered. Then you can have the remaining views fall into place in a way that's expected. If you need it scaled on a % for the screen size you can use a GeometryReader to scale the view for different screen sizes.
GeometryReader { reader in
Image("logo")
.resizeable()
.aspectRatio(contentMode: .fit)
.frame(width: reader.size.width * 0.2)
}
Finally, remove your ZStack and set it up like this.
VStack {
//Your Content
}.background(
//Make sure to set the edgesIgnoring.. on this NOT the VStack
Image("background").edgesIgnoringSafeArea(.all)
)
Tip When Working with Stacks
Stacks, wether VStack, HStack, or ZStack's all ALWAYS have their frame set to the content that is held inside of them. If you had an object with a width of 100 and a height of 10,000 then the Stack would also have those dimensions, unless otherwise specified with a Modifier such as .frame(width...) or even, in your case, an Image that is resized.
Suppose your same stack then has a width: 10, height: 10 view added to it, it would still retain the same size as the largest content held within. This is of course handled differently with HStack and VStacks as they actually stack things in a 2D plane, whereas the ZStack works on the 3D plane.

SwiftUI Image scaledToFill layout behaviour

In my iOS 14 Widget I want to display several circular images in a row.
When using scaledToFit() the image stretches weirdly to fit the circle. scaledToFill() gives me the desired output like so:
Image("Person")
.resizable()
.scaledToFill()
.clipShape(Circle())
But this changes the behaviour of the view to ignore it's parent and expand beyond it. Setting a fixed frame prevents this, but I need these Images to resize dynamically. When I place this View inside an HStack in my Widget the Images are way too large.
How can I get the image to scale like scaledToFill() and still respect the parent view.
You can try using geometry reader. This is an example code of what I have used in my Widget so you have to adjust it for your project, however, you will get the idea.
GeometryReader { geo in
ZStack(alignment: .bottom) {
HStack {
Image("Person")
.resizable()
.frame(width: geo.size.width, height: geo.size.height)
.aspectRatio(contentMode: .fit)
}
}
}
I found a solution in this post, the first answer works great in the Widget:
stackoverflow.com/questions/58290963/clip-image-to-square-in-swiftui
However I am reusing the view within my app and it caused some weird behaviour there. Instead I am now clipping the Image to a Circle before the View is created. This allows me to use .scaledToFit() and still maintain the original aspect ratio of the image.
I found it works best when wrapping the GeometryReader directly into the Image
HStack {
GeometryReader { geo in
Image("Person")
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
}
}

SwiftUI Finding Solution for Button overlay on Image

I overlayed a button/text on the image, but the button didnt respond. Is there any solution?
It seems like the hidden navigation bar view is blocking the button or the image is blocking the touch gesture. How should I fix this?
struct Tutorial: View {
let dataModel = DataModel()
#State var messageIndex = 0
var body: some View{
ZStack{
Image("Tutorial UI")
.resizable()
.scaledToFill()
.overlay(
VStack(spacing:20){
VStack(spacing:30){
HStack{
Spacer()
Text("Next")
.foregroundColor(.blue)
.fontWeight(.black)
.onTapGesture(count:1) {
if self.messageIndex != 4{
self.messageIndex += 1
}else{
self.messageIndex = 0
}
}
}
Text(dataModel.tutorialMessage[messageIndex][0])
.fontWeight(.black)
.font(.largeTitle)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
}.padding()
Text(dataModel.tutorialMessage[messageIndex][1])
.font(.title)
Spacer()
}.padding(.horizontal, 48)
.padding(.top, 50)
)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity,
alignment: .center)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
What #Asperi was getting at is you have provided a code snippet that depends upon a model we know nothing about. When you post code, you should post a simplified version. In this case I took your code and swapped out your data model for a simple array. I also downloaded an image to use from NASA's Image of the Day to use. And, Asperi nailed the problem with his second question...
Looking at it, the first thing that struck me is you are trying too hard to position things just so in SwiftUI. It doesn't work that way. And that led to your problems. I pulled out your frames which actually don't do anything in this layout. I also pulled out the specific padding amounts. I then pulled the Spacer() out that was in your HStack, and your "Next" button appeared on the screen. I suspect that setting the image as .scaledToFill() and an image that is not in the same dimensions of the screen caused the view to be larger than the screen you could see. Putting the Spacer() in caused your "Next" button to move all of the way to the right, which was off screen. Once I could see it, I was able to click it and it worked fine.
You have an extremely complicated structure that produces a pretty simple layout. Next time, start small and only add what you need. Less layout is better in SwiftUI, or you end up with unintended consequences. Try changing the HStack in your Overlay to the following and then work from there, one step at a time:
HStack{
Text("Next")
.foregroundColor(.blue)
.fontWeight(.black)
.onTapGesture(count:1) {
if self.messageIndex != 4{
self.messageIndex += 1
}else{
self.messageIndex = 0
}
}
}
Remember, in SwiftUI the Child View determines its own size. Don't try to force things from a Parent.