Forcing a View to the bottom right of View in SwiftUI - swift

I'm teaching myself SwiftUI by taking the 2020 version of Stanford's CS193p posted on YouTube.
I'd like to have a ZStack with a check box in the bottom right. This code puts the check mark
right in the middle of the View.
ZStack {
.... stuff ....
Text("✓")
}
Much to my surprise, this code puts the check mark in the top left corner of the View. (My mental model of GeometryReader is clearly wrong.)
ZStack {
.... stuff ...
GeometryReader { _ in Text("✓") }
}
I can't use ZStack(alignment: .bottomTrailing) { ... } as suggested in a different StackOverflow answer, as I want the alignment to apply only to the text, and not to anything else.
Is there any way to get the check mark on the bottom right? I'm trying to avoid absolute offsets, since I want my code to work no matter what the size of the ZStack view. Also, is there a good tutorial on layout so that I can figure out the answer to these questions myself?
[In case you're wondering. I'm playing around with Homework Assignment 3. I'm adding a "cheat mode" where if there is a matchingSet on the board, it marks them with a small check mark.]

Try applying
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
to Text.

I would say that it's possible if you know the size of the Text and the largest subview in the ZStack.
In the example below I've used 40 as the Text width and 20 as the Text height, and 200 (square) for the largest Color subview. You can then calculate the subview offsets using relative offsets and layout guides.
This is useful if you don't want to expand the Text to fill the container like the above answer.
struct ColorsView: View {
var body: some View {
ZStack(alignment: .center) {
Color.red.frame(width: 200,
height: 200,
alignment: .center)
Text("Bye")
Text("Hello").frame(width: 40, height: 20)
.alignmentGuide(HorizontalAlignment.center, computeValue: { dimension in
-60
})
.alignmentGuide(VerticalAlignment.center, computeValue: { dimension in
-80
})
}
}
}

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.

Using SwiftUI on watchOS, how to align views to the navigation title?

I would like to align some content to the same inset the the navigation title uses.
On older watch devices, there is no inset, but on newer devices with the rounded corners, the title is inset, and for certain things I would like to align to the same inset.
The apple docs talk about this margin, but I can't figure out how to use it.
How can I modify this example to have the left edge of the Text and Button align with the Navigation Title?
var body: some View {
VStack(alignment: .leading) {
Text("Hello World")
Button("Test") {}
}
.navigationTitle("Title")
}
Use .scenePadding(.horizontal). That will automatically apply the padding you want on the larger watches. Docs: https://developer.apple.com/documentation/swiftui/menu/scenepadding(_:)
Leading padding is simply (on the VStack):
.padding(.leading, 9.5)
but you have to do it based on the watch model.
Here is an example of how to determine the watch model:
How to determine the Apple Watch model?
And you might want to put this in a conditional view modifier:
https://stackoverflow.com/a/62962375/14351818
You could use the current screen width:
var screenWidth = WKInterfaceDevice.current().screenBounds.width
and apply that (minus the desired margin) as a frame width to your VStack:
VStack(alignment: .leading){
Text("Hello World")
Button("Test") {}
}
.frame(width: screenWidth - 25)
.navigationTitle("Title")
This worked fine on 45mm and 40mm, but unfortunately still not quite there on the 38mm watch, so you might want to explicitly set different paddings/frames for smaller devices.

why geometry reader doesn't center its child?

red border is geometry Area and black border is text area
currently using Xcode12 Beta 3
struct Testing_Geometry_: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.border(Color.black)
}
.border(Color.red)
}
}
I wanted to position text in center with this code
struct Testing_Geometry_: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
.border(Color.black)
}
.border(Color.red)
}
}
but I got this result which means Text is taking the whole geometry size and I think it's not correct!
cause texts has to fit in their space
three roles suggested by #twostraws for layout systems are
1- parent offers its size
2-child chooses its size
3-parent positions its child
but I think this isn't right!
text is taking the whole geometry space
If someone is looking for basic solution, you can put it in one of Stack and make it use whole size of geometry size with alignment center. That will make all elements underneath to use correct size and aligned in center
GeometryReader { geometry in
ZStack {
// ... some of your views
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
}
The problem is that modifier order matters, because modifiers actually create parent views. I've used backgrounds instead of borders because I think they're easier to see. Consider this code that's the same as yours but just using a background:
struct TestingGeometryView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
.background(Color.gray)
}
.background(Color.red)
}
}
This gives the following:
From this you are thinking "Text is taking the whole geometry size and I think it's not correct!" because the gray background is taking the whole screen instead of just around the Text. Again, the problem is modifier order- the background (or border in your example) is a parent view, but you are making it the parent of the "position" view instead of the Text view. In order for position to do what it does, it takes the entire parent space available (in this case the whole screen minus safe area). So putting background or border as parent of position means they will take the entire screen.
Let's switch the order to this, so that background view is only for the Text view and we can see size of Text view:
struct TestingGeometryView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.background(Color.gray)
.position(x:geo.frame(in:.global).midX,y:geo.frame(in:.global).midY)
}
.background(Color.red)
}
}
This gives the result I think you were expecting with the Text view only taking up the minimum size required, and following those rules that #twostraws explained so nicely.
This is why modifier order is so important. It's clear that GeometryReader view is taking up the entire screen, and Text view is only taking up the space it requires. In your example, Text view was still only taking up the required space but your border was around the position view, not the Text view. Hope it's clear :-)

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.

HStack inside ScrollView animates weirdly when opening view

So I was trying to implement expanding/contracting rows in a for each embedded in a list view and it turns out the list view cells don't animate smoothly. After checking out this tutorial, it suggests using a scrollview with a foreach because it animates smoother. The expanding/contracting works fine, but there unintended side effects only when I first open the page. The HStack appears to start flattened on the left side of the view and animates expanding to its normal starting position. I've slowed down the animations in the video so it's easier to see, and I removed the ForEach since it doesn't seem to be the cause of the problems. I don't know what is causing this and google searching has yielded no answers. Does anyone here have an answer or at least some advice? Much appreciated
#State var showTemp: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
HStack(alignment: .top) {
Text("Hello")
.font(.system(size: 32))
if showTemp {
VStack {
Text("Middle 1")
Text("Middle 2")
Text("Middle 3")
Text("Middle 4")
Text("Middle 5")
}
}
Spacer()
Text("Goodbye")
}
.border(Color.black, width: 2)
.onTapGesture {
self.showTemp.toggle()
}
.background(Color.blue)
.animation(.default)
}
.background(Color.green)
.onAppear(perform: loadMethod)
}
It seems I've answered my own question after I looked at the problem from a different angle. Using GeogetryReader and setting the frame width of the HStack to geometry.size.width fixed it.
I think this works because without specifying the width, the HStack fills as much area as it can. For some reason, it starts smushed when the view is loaded and then expands to fill its area, which I guess isn't visible unless you apply an animation to it.
Trying to be helpful here, I don't want to be one of those people who answers their own questions but doesn't show what the answer it.