Reputation: 7565
I'm trying to build a simple watchOS UI with SwiftUI with two pieces of information side-by-side above a button.
I'd like each side (represented as a VStack
within an HStack
) to take up half of the available width (so it's an even 50/50 split within the yellow parent view) divided where the | character is centered on the button in the example below.
I want the Short and Longer!!! text to each be centered within each side's 50%.
I started with this code, to get the elements in place and show the bounds of some of the different stacks:
var body: some View {
VStack {
HStack {
VStack {
Text("Short").font(.body)
}
.background(Color.green)
VStack {
Text("Longer!!!").font(.body)
}
.background(Color.blue)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.yellow)
Button (action: doSomething) {
Text("|")
}
}
}
Then, when it comes to making each side-by-side VStack
50% of the available width, I'm stuck. I thought it should work to add .relativeWidth(0.5)
to each VStack
, which should, as I understand it, make each VStack
half the width of its parent view (the HStack
, with the yellow background):
var body: some View {
VStack {
HStack {
VStack {
Text("Short").font(.body)
}
.relativeWidth(0.5)
.background(Color.green)
VStack {
Text("Longer!!!").font(.body)
}
.relativeWidth(0.5)
.background(Color.blue)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.yellow)
Button (action: doSomething) {
Text("|")
}
}
}
How can I get the behavior I want with SwiftUI?
Update: After reviewing the SwiftUI documentation more, I see the example here that sets a frame and then defines a relative width in comparison to that frame, so maybe I'm not supposed to use relativeWidth
in this way?
I'm a step closer to what I want with the following code:
var body: some View {
VStack {
HStack {
VStack {
Text("Short").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.green)
VStack {
Text("Longer!!!").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.blue)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.yellow)
Button (action: doSomething) {
Text("|")
}
}
}
which produces the following result:
Now, I am trying to figure out what's creating that extra space in the middle between the two VStack
s. So far, experimenting with getting rid of padding and ignoring safe areas does not seem to affect it.
Upvotes: 97
Views: 53779
Reputation: 61774
There is very simple solution:
HStack(alignment: .center, spacing: 0) {
ForEach(0...6, id: \.self) { _ in
Color(UIColor.white.withAlphaComponent(0.4))
.cornerRadius(5)
.padding(2)
.frame(minWidth: 0, maxWidth: .infinity)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 70)
and the result is the following:
Upvotes: 11
Reputation: 11
This is how I do it,
struct CenteredItemHStack: View {
var body: some View {
ZStack {
HStack {
Text("Leading Action")
.background(Color.blue)
Spacer()
}
Text("Center Action")
.background(Color.red)
HStack {
Spacer()
Text("Trailing Action")
.background(Color.blue)
}
}
.background(Color.green)
}
}
Upvotes: 1
Reputation: 2221
Here's how to create an EqualWidthHStack for watchOS 9, iOS 16, tvOS 16 & macOS 13
Here's the usage:
struct ContentView: View {
private let strings = ["Hello,", "very very very big", "world!"]
var body: some View {
EqualWidthHStack {
ForEach(strings, id: \.self) { string in
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.opacity(0.2)
Text(string)
.padding(10)
}
}
}
}
}
First create a struct
that conforms to Layout
.
struct EqualWidthHStack: Layout {
...
}
It will come with two default methods, here's how you can implement them.
Size that Fits:
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let totalSpacing = spacing.reduce(0.0, +)
return CGSize(width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
height: maxSize.height)
}
Place Subviews:
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let sizeProposal = ProposedViewSize(width: maxSize.width,
height: maxSize.height)
var x = bounds.minX + maxSize.width / 2
for index in subviews.indices {
subviews[index].place(at: CGPoint(x: x, y: bounds.midY),
anchor: .center,
proposal: sizeProposal)
x += maxSize.width + spacing[index]
}
}
You will need the following two helper methods.
Max Size:
private func maxSize(subviews: Subviews) -> CGSize {
let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
let maxSize: CGSize = subviewSizes.reduce(.zero, { result, size in
CGSize(width: max(result.width, size.width),
height: max(result.height, size.height))
})
return maxSize
}
Spacing:
private func spacing(subviews: Subviews) -> [CGFloat] {
subviews.indices.map { index in
guard index < subviews.count - 1 else { return 0.0 }
return subviews[index].spacing.distance(to: subviews[index + 1].spacing,
along: .horizontal)
}
}
Here's Apples WWDC22 video on how to make it:
Compose custom layouts with SwiftUI
Upvotes: 11
Reputation: 81
You have set background
of HStack
to yellow color and HStack
has some default inter child views spacing. By adding spacing: 0
in HStack
will solve the problem see the updated code below.
var body: some View {
VStack {
HStack(spacing: 0) { // Set spacing here
VStack {
Text("Short").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.green)
VStack {
Text("Longer!!!").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.blue)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.yellow)
Button (action: doSomething) {
Text("|")
}
}
}
Upvotes: 3
Reputation: 7565
I'm still confused about when and how relativeWidth
is supposed to be used, but I was able to achieve want I wanted without using it. (EDIT 18 July 2019: According to the iOS 13 Beta 4 release notes, relativeWidth
is now deprecated)
In the last update to my question I had some extra spacing between the two sides, and realized that was the default spacing coming in on the HStack
and I was able to remove that by setting its spacing to 0. Here's the final code and result:
var body: some View {
VStack {
HStack(spacing: 0) {
VStack {
Text("Short").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.green)
VStack {
Text("Longer!!!").font(.body)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.blue)
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.yellow)
Button (action: doSomething) {
Text("|")
}
}
}
Upvotes: 128