Aligning objects in ZStack - swift

The white bar is supposed to be aligned at the left of the darker bar. I've tried using spacers, or changing the alignment of the individual objects but nothing works. This is my code:
VStack {
HStack {
Text("Calories eaten today:")
.foregroundColor(.white)
.fontWeight(.semibold)
Spacer(minLength: 100)
HStack {
ZStack(alignment: .leading) {
Capsule()
.rotation(.degrees(90))
.frame(width: 20, height: CGFloat((calorieGoal/proportion)))
.foregroundColor(.black)
.opacity(0.2)
Capsule()
.rotation(.degrees(90))
.frame(width: 20, height: CGFloat((Swift.min((eatendatabase.dayNutrients[0]/proportion), 180))), alignment: .leading)
}
Spacer(minLength: 20)
Text("\(String(format: "%.0f", eatendatabase.dayNutrients[0]))")
.font(.caption)
.foregroundColor(.white)
.frame(alignment: .trailing)
}
Spacer()
}
}
.padding(.horizontal, 30)

As calorieGoal, proportional, and eatendatabase.dayNutrients were not provided as values, I have used the following constant and variable to keep track of the capsule's widths, which I will denote in the code as the "progress bar":
let MAX_WIDTH: CGFloat = 100.0
let currentProgress: CGFloat = 40.0
With that in mind, first you should consider moving the calorie count label ("832") outside of the HStack containing the progress bar. Second, you nested both the background and foreground capsules forming the progress bars in a ZStack. We can take the foreground capsule and align it to the left by nesting it in an HStack with a Spacer:
HStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: currentProgress /*This is the progress width of the bar, out of 100*/)
Spacer()
}
Of course, we need to set the width of the HStack to the width of the background capsule, so that the foreground capsule aligns properly with the leading edge of the background capsule:
HStack {
...
}.frame(width: MAX_WIDTH)
Overall, here is a possible solution to your issue:
VStack {
HStack {
Text("Calories eaten today:")
.foregroundColor(.white)
.fontWeight(.semibold)
// Made this font smaller for the preview
.font(.footnote)
// Add auto space between the label and the progress bar
Spacer()
// Separate the capsule progress bar from the calorie count
ZStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: MAX_WIDTH /*This is the max width of the bar*/)
.foregroundColor(.black)
.opacity(0.2)
// Nesting the progress Capsule in an HStack so we can align it to the left
HStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: currentProgress /*This is the progress width of the bar, out of 100*/)
// Adding a Spacer will force the Capsule to the left when it is in an HStack
Spacer()
}
// Set the frame width of the HStack to the same width as the background capsule
.frame(width: MAX_WIDTH)
}
// Add auto space between the progress bar and the calorie count
Spacer()
// The calorie count text, nested in an HStack with the label and the progress bar
Text("832")
.font(.caption)
.foregroundColor(.white)
.frame(alignment: .trailing)
}
}
.padding(.horizontal, 30)
// Added a gray background for the preview
.background(Color.gray)
And it would look like this:

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

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

Different verticalAlignment in HStack in SwiftUI

I'm trying to create simple cell layout in SwiftUI but I somehow stumbled on problem how to define different vertical alignments of elements in same HStack:
This is basically what I'm trying to achieve:
Whole view should be a cell, where there are some arbitrary paddings(24 on top, 20 at bottom). What is important is following:
HStack contains icon (red), vstack (title and description) and another icon (green)
Red icon should be aligned to the top of the HStack as well as vstack with texts
Green icon should be centered in the whole view
I've tried to achieve this with following code:
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 24) {
Image(nsImage: viewModel.icon)
.frame(width: 20.0, height: 20.0)
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.title)
Text(viewModel.text)
}
Spacer()
Image(nsImage: "viewModel.next")
}
.padding([.top], 24)
.padding([.bottom], 20)
Divider()
}
Without luck as obviously the green icon is also aligned to the top. I've tried to mess around with layout guides without success.
Another solution I've tried is
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 24) {
Image(nsImage: viewModel.icon)
.frame(width: 20.0, height: 20.0)
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.title)
Text(viewModel.text)
}
Spacer()
VStack {
Spacer()
Image(nsImage: "viewModel.next")
Spacer()
}
}
.padding([.top], 24)
.padding([.bottom], 20)
Divider()
}
which doesn't work either as I have more of these 'cells' in super view and their height is stretched to fill the superview.
Any idea how to achieve this?
I would treat the left-hand image and text as a single, top-aligned HStack, then put that in another HStack aligned centrally with the right-hand image. In shorthand, omitting spacing etc.:
HStack(alignment: .center) {
HStack(alignment: .top) {
Image(nsImage: ...)
VStack(alignment: .leading) {
Text(...)
Text(...)
}
}
Spacer()
Image(nsImage: ...)
}
That way, you only have a spacer working in the horizontal axis, so your overall vertical frame will be determined by the content alone.

How to move this to the top?

I just learned how to implement specific rounded corners, but now it seems nothing will align to the top of the screen, even with spacers. How can I get it to align to the top of the screen?
Additionally, I would like the green to ignore the top safe area, but it wasn't doing that earlier either.
import SwiftUI
struct Dashboard: View {
#State var bottomLeft: CGFloat = 25
var body: some View {
ZStack {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
VStack {
Text("Good Morning, Sarah")
.font(Font.system(size: 36))
.foregroundColor(.standaloneLabelColor)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
Text("We're glad to see you and hope you're doing well. Let's take on the day.")
.font(Font.system(size: 20))
.foregroundColor(.standaloneLabelColor)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color.appColor)
.cornerRadius(bottomLeft, corners: .bottomLeft)
.padding(.bottom, 10)
}
Spacer()
}
}
}
Thanks in advance.
You can set the alignment on the ZStack to .top. You can then also remove the Spacer.
A Spacer does nothing in a ZStack - since it's on its own layer. But setting the alignment on the ZStack will align all views to the top. If this is not what you want, you can also just put the Spacer in the inner VStack and the contents of that will all be aligned to the top also.
Code:
ZStack(alignment: .top) {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
/* ... */
}
}
Also, to ignore the safe area at the top when applying the rounded corners, you should clip the corners and use ignoresSafeArea() within background. This ensures that you are only ignoring the safe area for the background, and not the whole view. Do the following:
.background(
Color.appColor
.cornerRadius(bottomLeft, corners: .bottomLeft)
.ignoresSafeArea()
)
// .background(Color.appColor)
// .cornerRadius(bottomLeft, corners: .bottomLeft)
.padding(.bottom, 10)
I had to remove some bits from your code as they were producing errors on my end. Working with what I had, you needed to add a Spacer() below the appropriate VStack.
Since your content was stored in a VStack embedded in another VStack, the outer VStack was essentially where the entire view lived. Putting a Spacer beneath this pushes it up to the top of the screen.
You can additionally add padding to the top of the VStack to move the view lower if you do not want it touching the top of the screen.
Code below:
import SwiftUI
struct ContentView: View {
#State var bottomLeft: CGFloat = 25
var body: some View {
ZStack {
Color("background")
.edgesIgnoringSafeArea(.all)
VStack {
VStack {
Text("Good Morning, Sarah")
.font(Font.system(size: 36))
.foregroundColor(Color.blue)
.fontWeight(.semibold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
Text("We're glad to see you and hope you're doing well. Let's take on the day.")
.font(Font.system(size: 20))
.foregroundColor(Color.blue)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color.green)
.cornerRadius(bottomLeft)
.padding(.bottom, 10)
Spacer() //Added a spacer here
}
}
}
}

Making Button span across VStack

I currently have the following SwiftUI view:
HStack {
...
VStack {
TextField { ... }
SecureField { ... }
Button { ... }
}
...
}
I've added a .background(Color.green) to the Button, and as you can see, the view is very snug to the text.
I'm wondering if there's a way to adjust the width of the button so that it fills across VStack - something like a .fill mode for UIStackView.
The best way to do this is via .frame(maxWidth: .infinity)
https://developer.apple.com/documentation/swiftui/view-layout
If you want the button not to be centered you need to specify alignment.
e.g.: .frame(maxWidth: .infinity, alignment: .leading)
Button(action: handleSignInAction) {
Text("Sign In")
}
.frame(maxWidth: .infinity)
.background(Color.green)
Old answer from 2019:
You could use a HStack with a Text and Spacer to get a Button that fills the width of its parent:
Button(action: handleSignInAction) {
HStack {
Spacer()
Text("Sign In")
Spacer()
}
}.background(Color.green)
#d.felber's answer is almost complete, but you'd need a Spacer() on each side to center:
Button(action: {
// TODO: ...
}) {
HStack {
Spacer()
Text("Sign In")
Spacer()
}
}
.frame(maxWidth: .infinity)
Did the trick for me.
If you would like to stick to the method SwiftUI docs suggest you need to use GeometryReader and set the buttons width manually. Geometry reader updates its properties for different devices and upon rotation.
GeometryReader { geometry in
Button().frame(width: geometry.size.width, height: 100)
}
Use frame(maxWidth: .infinity) inside the Button like this:
Button(action: {...}) {
Text("Sign In")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.green)
Not like this, because the tappable area doesn't stretch:
Button(action: {}) {
Text("Sign In")
}
.frame(maxWidth: .infinity)
.background(Color.green)
Make the button text's frame the size of the UIScreen and then set the background color after it (make sure all style changes are done after changing the frame size, otherwise the style changes will only be visible on the original default frame). The frame size will propagate upward to increase the width of the button to the width of the screen as well.:
Button(action: {
// Sign in stuff
}) {
Text("Sign In")
.frame(width: UIScreen.main.bounds.width, height: nil, alignment: .center)
.background(Color.green)
}
You can also add some negative horizontal padding in between setting the frame and background in order to offset from the edge of the screen:
Button(action: {
// Sign in stuff
}) {
Text("Sign In")
.frame(width: UIScreen.main.bounds.width, height: nil, alignment: .center)
.padding(.horizontal, -10.0)
.background(Color.green)
}
Something like this?
Button(action: {
// Do your login thing here
}) {
Capsule()
.frame(height: 44)
.overlay(Text("Login").foregroundColor(Color.white)).padding()
}
I am not sure if there is a .fill method similar to UIStackView but, if what you want to do is provide some spacing on the Button (or any view for that matter) what worked for me is either setting the frame or padding
.frame(width: 300, alignment: .center) (we can also set a height here but, if not it should be able to infer the height based on the button text.
If you don't want to set an arbitrary width, you can also import UIKit and make use of UIScreen and get the devices full width. (There may be a SwiftUI way of getting this but, haven't found it at this time yet)
.frame(width: UIScreen.main.bounds.width, alignment: .center)
and then you can add a little bit of padding for some breathing room
.padding(.all, 20)
The issue with the padding is that it will add onto the additional width of the screen so we would have to take into account when setting the width.
(20 * 2 sides from leading and trailing)
.frame(width: UIScreen.main.bounds.width - 40, alignment: .center)
(breathing room is for when the text covers to the end of the screen or if your alignment is .leading or .trailing)