Override HStack alignment for one child - swift

Is there a way to dynamically override the alignment property of an HStack in an individual element?
Consider this scenario
There is a parent HStack with alignment = bottom
There are 3 elements inside the HStack of different sizes
I want the 3rd element to align to the top of the HStack. This alignment is different from the Hstack's bottom alignment
var body: some View {
HStack(alignment: .bottom) {
Rectangle()
.fill(.yellow)
.frame(height: 100)
Rectangle()
.fill(.blue)
.frame(height: 20)
// I want this to go to the top of the HStack
Rectangle()
.fill(.green)
.frame(height: 50)
}
.background {
Color.red
}
}
I'm trying to get the HStack to respect the highest height of 100 and just alter the last element's alignment.
I've tried wrapping the 3rd element in another stack but that only works if I specify a maxHeight equal to the tallest height among the parent's children, 100.
This means these rectangles have to know about their sibling elements.
HStack {
Rectangle()
.fill(.green)
.frame(height: 50)
}
.frame(maxHeight: 100, alignment: .top)

You could try this:
var body: some View {
HStack(alignment: .bottom) {
Rectangle()
.fill(.yellow)
.frame(height: 100)
Rectangle()
.fill(.blue)
.frame(height: 20)
VStack {
Rectangle()
.fill(.green)
.frame(height: 50)
Spacer()
}
}
.background {
Color.red
}
}
If, for some reason, you want to limit the range of how much space can the Spacer take up, you can add a modifier as per following example:
VStack {
Rectangle()
.fill(.green)
.frame(height: 50)
Spacer()
.frame(minHeight: 10, maxHeight: 50)
}

Related

SWIFTUI: .overlay breaks alignment

I have this bit of code below where I'm trying to have a rectangle centered in the screen, where I overlay a text outupt from some function and a share button. The concept works, except that when I add the .overlay, the "text box" (the rectangle with the text and share button overlayed to it) lose the centered alignment and they move to the left.If I remove the .overlay, the rectangle is centered but the content remains behind it. What am I doing wrong? Thanks.
GeometryReader { geometry in // Debug alignment
VStack(alignment: .center) { //TextBox
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.75, height: 200, alignment: .center)
.overlay(
ScrollView {
HStack(alignment: .top){
VStack(alignment: .center) {
Text(responseText)
.foregroundColor(.black)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.layoutPriority(2)
}
ShareLink(item: responseText){
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
}
}
}
)
} //Vstack Top
} //Geo Reader
I tried to center the recantgle while using the .overlay to put the conetent on top of it but the centered aligment gets lost. I also tried to use a ZStack instead of a .overlay but the content remains hidden behind.
GeometryReader positions its children at its top left. The VStack child of GeometryReader hugs its children—in this case, its single child, which is a Rectangle with a fixed frame.
Put the Rectangle in a container that expands to the size of the enclosing GeometryReader and centers the Rectangle within the expanded size.
For example, add a frame(maxWidth: .infinity, maxHeight: .infinity) modifier on the VStack:
GeometryReader { geometry in // Debug alignment
VStack {
Rectangle()
.fill(Color.cyan)
etc. etc.
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Result:
But if you don't have anything else to put in the VStack, you can remove it and put the frame modifier directly on the Rectangle, after the overlay:
GeometryReader { geometry in // Debug alignment
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.75, height: 200)
.overlay(
ScrollView {
HStack(alignment: .top){
Text(responseText)
.foregroundColor(.black)
ShareLink(item: responseText) {
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
}
}
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

mysterious unwanted space got added in Text view

It's simpler to just read the code and see the end result in the image, my question is why is Actuation Force and Bottom-out Force not aligned with the text views above them? In Actuation Force text view, there seems to be some mysterious padding added to its left while Bottom-out Force does not have it?
VStack {
VStack {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("**Pre-travel Distance**")
Text("how far down the key must be pressed for it to actuate")
}
.frame(width: 250)
.background(.red)
SwitchPropertyCircularView(upperBoundValue: theSwitch.preTravelDistance!, unit: "mm")
}
.background(.blue)
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("**Total Travel Distance**")
Text("how far down the key must be pressed for it to bottom out")
}
.frame(width: 250)
.background(.red)
SwitchPropertyCircularView(upperBoundValue: theSwitch.totalTravelDistance!, unit: "mm")
}
.background(.blue)
}
.frame(width: 380)
.background(Color.gray)
VStack {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("**Actuation Force**")
Text("the force needed to register a keypress")
}
.frame(width: 250)
.background(.red)
SwitchPropertyCircularView(upperBoundValue: Double(theSwitch.actuationForce!), unit: "gf")
}
.background(.blue)
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("**Bottom-out Force**")
Text("the force needed for a switch to bottom out")
}
.frame(width: 250)
.background(.red)
SwitchPropertyCircularView(upperBoundValue: Double(theSwitch.bottomOutForce!), unit: "gf")
}
.background(.blue)
}
.frame(width: 380)
.background(Color.gray)
}
On "Bottom-out Force," it looks like the first line is wide enough before it wraps words that it pushes the Text width to the maximum width of the view. On the others, it gets wrapped with shorter line lengths, and then ends up adding padding to center the text within the view.
You can fix this by adding a .leading alignment to your VStack. For example:
VStack(alignment: .leading, spacing: 5) {
Text("**Actuation Force**")
Text("the force needed to register a keypress")
}
.frame(width: 250, alignment: .leading) //<-- Here
Repeat for each VStack that shares the same qualities.

SwiftUI - LazyVGrid spacing

I have a LazyVGrid like so in SearchView.swift file
let layout = [
GridItem(.flexible()),
GridItem(.flexible())
]
NavigationView {
ZStack {
VStack {
....Some other stuff here
ScrollView (showsIndicators: false) {
LazyVGrid(columns: layout) {
ForEach(searchViewModel.allUsers, id: \.uid) { user in
NavigationLink(destination: ProfileDetailedView(userData: user)) {
profileCard(profileURL: user.profileURL, username: user.username, age: user.age, country: user.city)
}
}
}
}
}
}
}
My profileCard.swift looks like:
var body: some View {
ZStack {
Image.image(urlString: profileURL,
content: {
$0.image
.resizable()
.aspectRatio(contentMode: .fill)
}
)
.frame(width: 185, height: 250)
.overlay(
LinearGradient(gradient: Gradient(colors: [.clear, .black]), startPoint: .center, endPoint: .bottom)
)
.overlay(
VStack (alignment: .leading) {
Text("\(username.trimmingCharacters(in: .whitespacesAndNewlines)), ")
.font(.custom("Roboto-Bold", size:14))
.foregroundColor(Color.white)
+ Text("\(age)")
.font(.custom("Roboto-Light", size:14))
.foregroundColor(Color.white)
HStack {
Text("\(country)")
.font(.custom("Roboto-Light", size:14))
.foregroundColor(.white)
}
}
.padding(.leading, 15)
.padding(.bottom, 15)
,alignment: .bottomLeading
)
.cornerRadius(12)
}
}
This is producing 2 different spaces on different screen sizes:
iPhone 12:
iPhone 12 Pro Max
Im trying to get the same amount of spacing between the cards (horizontal and verticle) and the around the cards on all devices, any help on achieving this?
UPDATE
Following the example by #Adrien has gotten me closer to the problem, but when I use an image, the results change completely
let columns = Array(repeating: GridItem(.flexible(), spacing: 20, alignment: .center), count: 2)
ScrollView (showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(searchViewModel.allUsers, id: \.uid) { user in
NavigationLink(destination: ProfileDetailedView(userData: user)) {
HStack {
Image("placeholder-avatar")
.resizable()
.aspectRatio(contentMode: .fill)
}
.frame(maxWidth: .infinity, minHeight: 240) // HERE 1
.background(Color.black)
.cornerRadius(25)
}
}
}.padding(20)
}
The array of GridItem only fixes the behavior of the container of each cell. Here it is flexible, but that does not mean that it will modify its content.
The View it contains can have three different behaviors :
it adapts to its container:
like Rectangle, Image().resizable(), or any View with a .frame(maxWidth: .infinity, maxHeight: .infinity)) modifier.
it adapts to its content (like Text, or HStack).
it has a fixed size (as is your case with .frame(width: 185, height: 250))
If you want the same spacing (vertical and horizontal) between cells, whatever the device, the content of your ProfileDetailedView must adapt to its container :
you have to modify your cell so that it adopts behavior 1.
you can use the spacing parameters of yours GridItem (horizontal spacing) and LazyVGrid (vertical).
Example:
struct SwiftUIView5: View {
let columns = Array(repeating: GridItem(.flexible(), spacing: 20, alignment: .center), count: 2) // HERE 2
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) { // HERE 2
ForEach((1...10), id: \.self) { number in
HStack {
Text(number.description)
.foregroundColor(.white)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, minHeight: 200) // HERE 1
.background(Color.black)
.cornerRadius(25)
}
}
.padding(20) // HERE 2
}
}
}
Actually, the spacing is a function of the screen size, in your case. You have hard-coded the size of the card (185x250), so the spacing is as expected, a bit tighter on smaller screens, bit more airy on larger screens.
Note that the vertical spacing is consistent and as expected. This seems logical because there is no vertical size constraint.
BTW, the two screenshots are not the same size, otherwise this aspect would be more obvious.
If you want to keep the size of the card fixed, you can perhaps at least keep the horizontal space between them constant, but then you will have larger margins on the sides.
Or you can adjust the size of the card dynamically. Check out GeometryReader, which will help you access runtime screen sizes.

Prevent view from overflowing in both directions

I'm trying to build a user-expandable view that will show more of its content by increasing its height. To accomplish this I will add .clipped() so that the content shown out of its bounds will be hidden, just like you would add overflow: hidden; in CSS.
However, it seems like by default VStack is centering its children, so when the height is smaller than the sum of the children's height, they overflow in both the top and the bottom.
Here is an example of what I'm talking about:
struct ExandableView: View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
.frame(height: 50)
.padding(.horizontal)
Rectangle()
.fill(Color.green)
.frame(height: 50)
.padding(.horizontal)
}
.frame(height: 90)
.background(Color.blue)
}
}
Is there any way to make it behave so that the red item (the first) is always inside the blue container and the children can only overflow from the bottom?
Change frame alignment according to ExandableView. like this
struct ExandableView: View {
var body: some View {
// Other VStack code
}
.frame(height: 90, alignment: isExpand ? .top : .center) //<--Here
.background(Color.blue)
}
}

Is it possible to get the calculated "minimum" height for a VStack in Swift UI

I'm working with a card-like view, and I would essentialy like to know the minimum height needed to house the elements, and then add a border around that.
this becomes difficult since VStack and HStack seem to want to take up the most space possible, so I need to manually insert a .frame(...) to coercse this to work, but that makes it very unflexible.
In the picture I'm highlighting the VStack which, as you can see, is overfilling beyond the constraints of the 48px frame height.
code
var body: some View {
HStack(spacing: 0) {
Image("cocktail")
.resizable()
.scaledToFit()
.padding(Size.Spacing.unit * 2)
.background(self.color)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(self.color)
)
HStack(spacing: 0) {
VStack(alignment: .leading) {
Text("Negroni").font(.headline)
Spacer()
HStack {
Tag(name: "bitter")
Tag(name: "sweet")
Tag(name: "strong")
}
}
.padding(.leading, 10)
Spacer()
}
}
.padding(Size.Spacing.unit * 2)
.frame(height: 48.0)
}
maybe i misunderstand you, but i tried your example...unfortunately it was not compilable, so i had to change some things. but as you can see the result (vstack - yellow) is not taking to much space, maybe because of your padding?
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Image("cocktail")
.resizable()
.scaledToFit()
// .padding(Size.Spacing.unit * 2)
.background(Color.blue)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.yellow)
)
HStack(spacing: 0) {
VStack(alignment: .leading) {
Text("Negroni").font(.headline)
Spacer()
HStack {
Text("bitter")
Text("sweet")
Text("strong")
}
}
.background(Color.yellow)
.padding(.leading, 10)
Spacer()
}.background(Color.red)
}
// .padding(Size.Spacing.unit * 2)
.frame(height: 48.0)
}
}
since VStack and HStack seem to want to take up the most space possible
This assumption is incorrect - by default stacks are tight to content and content tight to minimum with default padding between UI elements. In provided code it is Spacer() responsibility on expanding to max available space. So to make minimum space for content, highlighted VStack should be changed as follows:
VStack(spacing: 0, alignment: .leading) {
Text("Negroni").font(.headline)
HStack {
Tag(name: "bitter")
Tag(name: "sweet")
Tag(name: "strong")
}
}