SwiftUI: disable interactions on transparent parts of ScrollView - swift

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.

Related

SwiftUI Fixed constraint from bottom of view to top of screen

Question: How to set fixed "constraint" from buttom of view to top of the screen?
My problem explanation:
I'm trying to implement such element on screen:
Rounded shape which has stable height in all screens
I use Oval to draw it like
public var body: some View {
VStack {
ZStack(alignment: .bottom) {
Ellipse()
.fill(Color.surfacePrimary)
.frame(width: 546, height: 364)
.offset(x: 0, y: -20)
Text("Text")
.padding(.bottom, 60)
.foregroundColor(.onPrimary)
}
.frame(minWidth: 0, idealWidth: .infinity, maxHeight: 20)
.edgesIgnoringSafeArea(.top)
Text("This text should be right below yellow shape, and it is, but on screens with not rectangle forms, on screens with rectangle form, it pops up on yellow zone")
Spacer()
}
}
Problem: that it works good on iphones 11, 12. But on 8, 8+, SE (where screens have right rectangle form) frame of ZStack move on top but Ellipse left on the same place. And Views which should be below yellow zone move to yellow zone.
UPD: I solved problem with different heights of StatusBar, but now I have another problem which was not observable before.
My new code:
public var body: some View {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
Text("Text")
.padding(.bottom, 42)
.foregroundColor(.red)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.background(Color.red)
}
This ZStack fills almost all height of the screen. But I expect it should has fixed height. It looks like:
(hmmm ... some problem with uploading image)

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.

How to use GeometryReader within a LazyVGrid

I'm building a grid with cards which have an image view at the top and some text at the bottom. Here is the swift UI code for the component:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
This component outputs the following layout:
This Looks great, but I want to add a Geometry reader into the Card component in order to scale the top image view according to the width of the enclosing grid column. As far as I know, that code should look like the following:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
GeometryReader { geometry in
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
The trouble is that this renders as the following:
As you can see, I'm not even trying to use the GeometryReader, I've just added it. If I add the geometry reader at the top level, It will render the grid correctly, however this is not of great use to me because I plan to abstract the components into other View conforming structs. Additionally, GeometryReader seems to be contextually useful, and it wouldn't make sense to do a bunch of math to cut the width value in half and then make my calculations from there considering the geometry would be from the top level (full width).
Am I using geometry reader incorrectly? My understanding is that it can be used anywhere in the component tree, not just at the top level.
Thanks for taking a look!
I had the same problem as you, but I've worked it out. Here's some key point.
If you set GeometryReader inside LazyVGrid and Foreach, according to SwiftUI layout rule, GeometryReader will get the suggested size (may be just 10 point). More importantly, No matter what subview inside GeometryReader, it wouldn't affect the size of GeometryReader and GeometryReader's parent view.
For this reason, your view appears as a long strip of black. You can control height by setting GeometryReader { subView }.frame(some size),
Generally, we need two GeometryReader to implement this. The first one can get size and do some Computing operations, then pass to second one.
(Since my original code contains Chinese, it may be hard for you to read, so I can only give a simple structure for you.)
GeometryReader { firstGeo in
LazyVGrid(columns: rows) {
ForEach(dataList) { data in
GeometryReader { secondGeo in
// subview
}
.frame(width: widthYouWantSubViewGet)
}
}
}
I just started to learn swift for a week. There may be some mistakes in my understanding. You are welcome to help correct it.

SwiftUI - setting the frame of view

When I learn new stuff like SwiftUI (beta 6)
I want to start from the basic.
I just want to set frame to subview like in UIKit.
What I'm missing here ? (this is from Simulator)
1. the subview is not in 0,0 position.
2. why at least the start of the word is not inside the border ?
UPDATE :
how to set the text view in 0,0 position ? (just like on UIKit)
I thought my question is very clear, but for some reason, it's not.
I think it's important to understand why your solution doesn't work because at a first glance it seems correct and it seems that SwiftUI works in some weird ways (because, of course, we are all used to UIKit).
You tried:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
.position(CGPoint(x: 0, y: 0))
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
}
And you got:
First of all, the position modifier says:
Fixes the center of the view at the specified point in its parent’s
coordinate space.
Two things are important here:
The view is moved based on its centre, not based on its top-left corner
The view is moved in the parent's coordinate space
But who is the Text's parent? A view modifier in SwiftUI is something that applies to a View and returns a View. Modifiers are applied from the last one to the first one (in reverse order respect to how you see them). In your case:
So: The centre of the Text is positioned at (0,0) respect to a Frame 50x100 with a red Border. The resulting View is placed in the centre of the screen because of the VStack (it's the VStack default behaviour). In other words: the position's parent (position returns a View, every modifier returns a View) is the Frame 50x100 placed in the centre of the screen.
If you want to position the top-left corner of the Text at (0,0) in the Frame coordinate space you should use the Spacer modifier this way:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Spacer()
}
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
And you'll get:
If you want, instead, the top-left corner of the Frame to be at (0,0) respect to the whole View I think the simplest way is:
struct ContentView: View {
var body: some View {
HStack {
VStack {
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
Spacer()
}
Spacer()
}
.edgesIgnoringSafeArea(.all)
}
}
And you'll get:
Do Like this way
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
}
}
Below is output
if you want to remove space on top add .edgesIgnoringSafeArea(.top) for your view like below
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
.edgesIgnoringSafeArea(.top)
}
}

SwiftUI: Increase tap/drag area for user interaction

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