AlbertUI
AlbertUI

Reputation: 1517

Get and set max widths between multiple SwiftUI views

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

Upvotes: 1

Views: 1360

Answers (3)

AlbertUI
AlbertUI

Reputation: 1517

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.

Upvotes: 1

nicksarno
nicksarno

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:

enter image description here

Upvotes: 1

Dabble
Dabble

Reputation: 297

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]

Upvotes: 1

Related Questions