How to override the alignment of a VStack if all component don't have fixed frames? - swift

I have an HStack, aligned top, with 3 components. I want the first Text to be pushed to the bottom, essentially overriding the HStack's alignment.
I want the Text to keep its intrinsic size and override its parent's alignment.
HStack(alignment: .top, spacing: 0) {
Text("BOTTOM")
.font(.system(size: 24))
.background {
Color.yellow
}
Spacer()
Text("TOP ")
.background {
Color.blue
}
Spacer()
Text("X")
.font(.system(size: 40))
.background {
Color.green
}
}
.background {
Color.red
}
I can wrap the Text in a VStack and add a Spacer before it but that will only work if
there is a fixed frame height on the Text OR
there is a fixed frame height on the HStack
If I don't have either and still add a Spacer, the Spacer stretches to take up the entire screen height.

Create two HStacks, the outermost aligned to the bottom, while an inner one wraps the second block, aligning everything to the top.
Here:
// Align the whole block to the bottom
HStack(alignment: .bottom, spacing: 0) {
Text("BOTTOM")
.font(.system(size: 24))
.background {
Color.yellow
}
// The sub-block is aligned to the top: create
// a new HStack
HStack(alignment: .top) {
Spacer()
Text("TOP ")
.background {
Color.blue
}
Spacer()
Text("X")
.font(.system(size: 40))
.background {
Color.green
}
}
}
.background {
Color.red
}

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

SwiftUI Centering Image between Views

I have an HStack that I am using as a tool bar. The HStack contains a button on the left, image in the middle, and then another HStack containing 2 buttons. Everything looks fine except for the image in the middle, it's slightly to the left. How can I get the image centered?
var toolbar: some View {
HStack(alignment: .center) {
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.tint(.black)
}
Spacer()
Image("nav_logo")
.resizable()
.frame(width: 30, height: 30)
Spacer()
optionsButton
}
.frame(height: 30)
}
var optionsButton: some View {
HStack {
Button {
UIPasteboard.general.string = viewModel.take.shareLinkString
self.buildBottomAlert(type: .linkCopied)
} label: {
Image("shareIcon")
}
.padding(.trailing, 10)
Button {
showingAlert = true
} label: {
Image("moreMenu")
}
}
The image in the middle is offset because the first button and optionsButton do not have the same width. But the SpacerĀ“s inside the containing Stack do have equal width.
To solve this you could wrap them in to a container that is big enough so both have the same width.
e.g.:
//GeometryReader to get the size of the container this is in
GeometryReader{proxy in
HStack(alignment: .center) {
//Hstack to wrap the button
HStack(){
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.tint(.black)
}
// Spacer to move the button to the left
Spacer()
}
// depending on the use case you would need to tweak this value
.frame(width: proxy.size.width / 4)
Spacer()
Image(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")
.resizable()
.frame(width: 30, height: 30)
Spacer()
HStack(){
Spacer()
optionsButton
}
.frame(width: proxy.size.width / 4)
}
.frame(height: 30)
}

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.

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.

Aligning objects in ZStack

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: