Reputation: 1517
Xcode 12.3 | SwiftUI 2.0 | Swift 5.3
I have multiple HStack
whose first element must have the same width, but I cannot know how to achieve this.
HStack {
Text("Label 1:")
Text("Random texts")
}
HStack {
Text("Label 2 Other Size:")
Text("Random texts")
Text("Additional text")
}
HStack {
Text("Label 3 Other Larger Size:")
Text("Random texts")
}
This will display this:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
And I want to display this without using VStacks
, because each HStack
is a List Row
:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
[__________________________] [_____________...
same size
I tried using a @propertyWrapper
that stores the GeometryProxy
of each Label's background
and calcs the max WrappedValue
in order to set the .frame(maxWidth: value)
of each Label
, but without success, I cannot have working that propertyWrapper (I get a loop and crash).
struct SomeView: View {
@MaxWidth var maxWidth
var body: some View {
HStack {
Text("Label 1:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
Text("Random texts")
}
HStack {
Text("Label 2 Larger Size:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
// Continue as the example above...
...
...
Upvotes: 1
Views: 1360
Reputation: 1517
Based on all responses I coded a simpler way to get this working with a modifier.
First, we have to add the necessary extensions with the view modifier and the width getter:
extension View {
func balanceWidth(store width: Binding<CGFloat>, alignment: HorizontalAlignment = .center) -> some View {
modifier(BalancedWidthGetter(width: width, alignment: alignment))
}
@ViewBuilder func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct BalancedWidthGetter: ViewModifier {
@Binding var width: CGFloat
var alignment: HorizontalAlignment
func body(content: Content) -> some View {
content
.background(
GeometryReader { geo in
Color.clear.frame(maxWidth: .infinity)
.onAppear {
if geo.size.width > width {
width = geo.size.width
}
}
}
)
.if(width != .zero) { $0.frame(width: width, alignment: .leading) }
}
}
With this, all the work is done. In order to get equal widths between views all we have to do is mark each view with the balancedWidth
modifier and store the shared width value in a @State
variable with initial value == .zero
:
@State var width: CGFloat = 0 <-- Initial value MUST BE == .zero
SomeView()
.balanceWidth(store: $width)
AnotherViewRightAligned()
.balanceWidth(store: $width, alignment: .leading)
struct ContentView: View {
@State var width: CGFloat = 0
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Label 1")
.balanceWidth(store: $width, alignment: .leading)
Text("Textss")
}
HStack {
Text("Label 2: more text")
.balanceWidth(store: $width, alignment: .leading)
Text("Another random texts")
}
HStack {
Text("Label 3: another texts")
.balanceWidth(store: $width, alignment: .leading)
Text("Random texts")
}
}
.padding()
}
}
We can create more relationships between views and balance the widths between them separately by creating more than one @State
variable.
Upvotes: 1
Reputation: 4245
The easiest way would be to set a specific .frame(width:) for the first Text() within the HStack. You can then use .minimumScaleFactor() to resize the text within to avoid the words being cut off. Here's an example to get you started:
struct ContentView: View {
var body: some View {
VStack {
CustomRow(title: "Label 1:", otherContent: ["Random Texts"])
CustomRow(title: "Label 2 Other Size::", otherContent: ["Random Texts", "Additional Text",])
CustomRow(title: "Label 3 Other Larger Size:", otherContent: ["Random Texts"])
}
}
}
struct CustomRow: View {
let title: String
let otherContent: [String]
var body: some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
.lineLimit(1)
.minimumScaleFactor(0.5)
ForEach(otherContent, id: \.self) { item in
Text(item)
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Result:
Upvotes: 1
Reputation: 297
You can pass a GeometryReader width into any child view by using a standard var (not @State var).
Try this code out, its using GeometryReader and a list of Identifiable items which are passed a "labelWidth" they all render the same width for all labels. Note: I used a VStack in the row so it looked better, didn't follow why a row can't have a VStack in it.
[![struct Item : Identifiable {
var id:UUID = UUID()
var label:String
var val:\[String\]
}
struct ItemRow: View {
var labelWidth:CGFloat = 0
var item:Item
var body: some View {
HStack(spacing:5) {
Text(item.label)
.frame(width:labelWidth, alignment: .leading)
VStack(alignment: .leading) {
ForEach(0 ..< item.val.indices.count) { idx in
Text(item.val\[idx\])
}
}
}
}
}
struct ContentView : View {
@State var items:\[Item\] = \[
Item(label:"Label 1:", val:\["Random texts"\]),
Item(label:"Label 2 Other Size:", val:\["Random texts","Additional text"\]),
Item(label:"Label 3 Other Larger Size:", val:\["Random texts", "Random texts and texts", "Random texts", "Random texts"\])
\]
var body: some View {
GeometryReader { geo in
let labelWidth = geo.size.width * 0.6 // set this however you want to calculate label width
List(items) { item in
ItemRow(labelWidth: labelWidth, item: item)
}
}
}
}][1]][1]
Upvotes: 1