skip to Main Content

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 WrappedValuein 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...
                ...
                ...

3

Answers


  1. Chosen as BEST ANSWER

    Balanced widths between views

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

    Usage

    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)
    
    

    Sample

    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.


  2. enter image description hereYou 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]
    
    Login or Signup to reply.
  3. 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:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search