SwiftUI - LazyVGrid spacing - swift

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.

Related

Override HStack alignment for one child

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

Having multiline text in a horizontal scroll view in SwiftUI

I want to make a UI that loads an array of strings into a scrollable horizontal stack. The texts however should have a max width of 100, and expand to be multiline to fill the space. This works, however the text is cut off and the horizontal ScrollView is only allowing the height for one line of text.
How do I let the ScrollView expand vertically to whatever the largest text in the stack is?
I want to have a control right above the stack, so setting the height to .infinity isn't a solution, and preferably don't want to have to work with GeometryReader.
My code so far:
VStack {
Capsule()
.foregroundColor(.black.opacity(0.5))
.frame(width: 48, height: 2)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(chats, id: \.self) { chat in
Text(chat)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: 100)
}
}
}
The result:
Change .frame(maxWidth: 100) to .frame(width: 100). You'll then need to put a maxHeight on your HStack lest it grow too tall.
VStack {
Capsule()
.foregroundColor(.black.opacity(0.5))
.frame(width: 48, height: 2)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach(chats, id: \.self) { chat in
Text(chat)
.frame(width: 100)
}
}
.frame(maxHeight: 200, alignment: .top)
}
}
Result:

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.

Spacer() swiftui leaves a white space

I'm currently building a UI that has a big red 250px height box on the top that holds information and a scrollview underneath it. I build this all in a VStack(). I put a spacer() at the bottom of the VStack to push everything to the top of the screen. But it has a big white space between the red container at the top and the scrollview. Even though both don't have any padding(). Does anyone know how this is possible? This is my code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack {
...
code
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
// this Spacer should push everything to the top
Spacer()
}
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Edit:
To make my question better understandable I made this summery:
I have build a VStack with 2 items in it. there is a big white space between the two and I don't know how to get rid of this white space. If I remove .ignoresSafeArea() the white space disappears. The thing is I need .ignoresSafeArea() to push the first item in VStack up. Does anyone know what I can do to keep .ignoresSafeArea() but get rid of the white space?
I added a screenshot of the problem. I gave the scrollview a green background so that the white spacing between the VStack and the Scrollview is visable.
You need to embed everything in a VStack with 0 spacing.
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack {
Text("Hello World!") //Easier to test
}
.frame(height: 250) //Dont use UIScreen
.background(Color.red)
.ignoresSafeArea()
// somehow it has a white space here...
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach((0...56), id: \.self) {_ in
Image("testimage")
.resizable()
.scaleEffect(CGSize(width: 1.0, height: 2.0))
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.cornerRadius(10)
}.padding(.bottom, 75)
}
.frame(width: UIScreen.main.bounds.width)
.padding(.top, -10)
}
Spacer()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
}
Also, you might need to give the ScrollView a max height to push it upwards. .frame(maxHeight: 175)
As you haven't given a reproducible example or an image. I cant tell you for certain what will work
I guess Arnavs Answer is correct.
Notice:
When you are trying to implement multiple Views on one page, 90% of the time it is better to have them in one H/V/Z-Stack.
For a better understanding start a new project and try ...
Navigationview{
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
}
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
and the same thing but "different"
NavigationView {
VStack{
Text("Hello Vertical World")
Text("Hello Vertical World2")
HStack{
Text("Hello Horizontal World")
Text("Hello Horizontal World2")
}
}
}
I can't double check bc i am on my phone, but this should look very different.
I fixed the issue by putting the VStack and the Scrollview in an other VStack in the navigationView. Then I put the .ignoresSafeArea() on this new VStack.
Code:
import SwiftUI
struct User: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationView {
// new VStack that includes the topbar VStack, the
// scrollview and has the .ignoresSafeArea() on it.
VStack {
VStack {
...
Code topbar
...
}
.frame(width: UIScreen.main.bounds.width, height: 250)
.background(Color.red)
Scrollview {
...
Code scrollview
...
}
Spacer()
}
.ignoresSafeArea()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}

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