Home > front end >  Complex alignment situation in SwiftUI
Complex alignment situation in SwiftUI

Time:09-27

I have a complicated alignment situation, which I was hoping there was some solution with alignmentGuides, but I can't figure it out.

I need to align the following list of entries (in a mono font), such that as a group it is horizontally centered.

But also that it is positioned the same way, and aligned the same way along the leading edge, even if one of the entries in a row is longer:

I don't want to hardcode any sizing or positioning values.

Here's some simple code to replicate:

struct TestView: View {

    let values: [(String, String)] = [
        ("03", "30.123"),
        ("02", "33.222"),
        ("01", "completed")
    ]

    var body: some View {
        LazyVStack {
            ForEach(values, id: \.1) { tuple in 
                HStack(spacing: 24) {
                    Text(tuple.0)
                    Text(tuple.1)
                }
            }
        }
        .frame(width: 350, height: 250) // to simulate outer container dimensions
    }
}

CodePudding user response:

Here is a simple way may it help:

struct ContentView: View {
    
    var body: some View {
        
        TestView()

    }
    
}

struct TestView: View {
    
    let values: [(String, String)] = [
        ("03", "30.123"),
        ("02", "33.222"),
        ("01", "completed")
    ]
    
    var body: some View {
        
        HStack(spacing: 24) {
            
            VStack { ForEach(values, id: \.1) { tuple in Text(tuple.0) } }

            VStack(alignment: .leading) { ForEach(values, id: \.1) { tuple in

                Text("00.000")
                    .foregroundColor(Color.clear)
                    .overlay(Text(tuple.1).lineLimit(1).fixedSize(), alignment: .leading)
  
            } }
            
        }
        .font(Font.system(.body, design: .monospaced))
        .padding(5.0)
        .background(Color.pink.opacity(0.5).cornerRadius(5.0))

    }
}

enter image description here

CodePudding user response:

I found a way to solve this, though it seems quite convoluted to me. Posting here, but still would love a better solution.

The idea was to use a placeholder with dummy content (relying on the fact that this was a monospaced font) and using anchorPreference to align around its leading edge.

struct LeadingPreferenceKey: PreferenceKey {
   static var defaultValue: Anchor<CGPoint>? = nil
    
   static func reduce(value: inout Anchor<CGPoint>?, 
                      nextValue: () -> Anchor<CGPoint>?) {
      value = nextValue()
   }
}

Then, set that in anchorPreference, and capture it in overlayPreferenceValue and position with offset(x:)

LazyVStack(alignment: .center) {
   row(("00", "00.000")) // placeholder that's center-positioned
      .opacity(0)
      .anchorPreference(key: LeadingPreferenceKey.self,
                        value: .leading, , transform: { $0 })
}
.overlayPreferenceValue(LeadingPreferenceKey.self) { leading in
   GeometryReader { geo in 
      LazyVStack(alignment: .leading) {
         ForEach(values, id: \.1) { tuple in
            row(tuple)
         }
      }
      .offset(x: leading.map { geo[$0].x } ?? 0)
   }
}
func row(_ tuple: (String, String)) -> some View {
   HStack(spacing: 24) {
      Text(tuple.0)
      Text(tuple.1)
   }
}

CodePudding user response:

Here is a solution that will provide your alignment as well as control of the width of your columns. It is a separate view that takes a tuple and returns two VStacks in an HStack contained by width:

struct TwoItemLeadingAlignedColumn: View {
    let firstColumnItems: [String]
    let secondColumnItems: [String]
    let width: CGFloat?
    
    init(items: [(String, String)], width: CGFloat? = nil) {
        firstColumnItems = items.map { $0.0 }
        secondColumnItems = items.map { $0.1 }
        self.width = width
    }
    
    var body: some View {
        GeometryReader { geometry in
            HStack {
                VStack(alignment: .center) {
                    ForEach(firstColumnItems, id: \.self) {item in
                        Text(item)
                    }
                }
                
                Spacer()
                    .frame(width: geometry.size.width * 0.25)
                
                VStack(alignment: .leading) {
                    ForEach(secondColumnItems, id: \.self) {item in
                        Text(item)
                    }
                }
                Spacer()
            }
        .frame(width: width)
        }
    }
}
    

struct TestView: View {
    
    let values: [(String, String)] = [
        ("03", "30.123"),
        ("02", "33.222"),
        ("01", "completed")
    ]
    
    var body: some View {
        TwoItemLeadingAlignedColumn(items: values, width: 150)
            .frame(width: 350, height: 250) // to simulate outer container dimensions
    }
}

This answer will work whether or not you are using a monospaced font.

Update: Center first column

  • Related