1

The code below is using a LazyVGrid to implement layout of my controls such that everything lines up like this:

enter image description here

Particularly, the sliders' ends all align and the symbols are centre-aligned to each other. I have worked out how to size the GridItems for the symbol and the numeric readout such that they are the 'right size' for their contents — something neither flexible nor adaptive GridItems will do — while accounting for Dynamic Type.

In testing, this works really well so long as the user does not change their Dynamic Type size once the app is active. If that is done, the type (and symbols) will adapt as they should, but the GridItem size remains fixed at its initial value. This causes the numbers to wrap at the decimal point.

Is there a way to have the GridItem resize in response to Dynamic Type changes, or is there a better way to do this layout?

import SwiftUI

struct ProtoGrid: View {
   let gridItems = [
      GridItem(.fixed(UIImage(systemName: "ruler", withConfiguration: UIImage.SymbolConfiguration(textStyle: .body, scale: .large))!.size.width)),
      GridItem(.flexible(minimum: 40, maximum: .infinity)),
      GridItem(.fixed(("00.00" as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body)]).width + 4), alignment: .trailing)
   ]
   @State var index1 = 5.0
   @State var index2 = 5.0
   @State var index3 = 5.0
   var body: some View {
      VStack {
         Rectangle()
            .fill(Color.red)
         LazyVGrid(columns: gridItems, spacing: 12) {
            Image(systemName: "person").imageScale(.large)
            Slider(value: $index1, in: 0...10)
            Text("\(String(format: "%.2f", index1))").font(Font.system(.body).monospacedDigit())
            Image(systemName: "megaphone").imageScale(.large)
            Slider(value: $index2, in: 0...10)
            Text("\(String(format: "%.2f", index2))").font(Font.system(.body).monospacedDigit())
            Image(systemName: "ruler").imageScale(.large)
            Slider(value: $index3, in: 0...10)
            Text("\(String(format: "%.2f", index3))").font(Font.system(.body).monospacedDigit())
         }
         .padding()
      }
   }
}

struct ProtoGrid_Previews: PreviewProvider {
    static var previews: some View {
        ProtoGrid()
         .previewDevice("iPhone 11 Pro")
    }
}
111
zkarj
  • 617
  • 9
  • 18

1 Answers1

0

It seems clear the purpose of your try to use grid here, to have aligned sliders due to different image sizes, but grid configuration is constant, ie you did it once. And it is pretty hardcoded, actually, so does not appropriate for dynamic text case.

I would propose alternate approach - just use regular HStack, which fits content dynamically, and some custom dynamic alignment for content.

Tested with Xcode 12 / iOS 14

demo

struct ProtoGrid: View {

    @State var index1 = 5.0
    @State var index2 = 5.0
    @State var index3 = 5.0

    @State private var imageWidth = CGFloat.zero

    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.red)
            HStack(spacing: 12) {
                Image(systemName: "person").imageScale(.large)
                    .alignedView(width: $imageWidth)
                Slider(value: $index1, in: 0...10)
                Text("\(String(format: "%.2f", index1))").font(Font.system(.body).monospacedDigit())
            }
            HStack(spacing: 12) {
                Image(systemName: "megaphone").imageScale(.large)
                    .alignedView(width: $imageWidth)
                Slider(value: $index2, in: 0...10)
                Text("\(String(format: "%.2f", index2))").font(Font.system(.body).monospacedDigit())
            }
             HStack(spacing: 12) {
                Image(systemName: "ruler").imageScale(.large)
                    .alignedView(width: $imageWidth)
                Slider(value: $index3, in: 0...10)
                Text("\(String(format: "%.2f", index3))").font(Font.system(.body).monospacedDigit())
            }
        }
        .padding()
    }
}

extension View {
    func alignedView(width: Binding<CGFloat>) -> some View {
        self.modifier(AlignedWidthView(width: width))
    }
}

// creates a view which uses max width of calculated intrinsic
// content or shared from external width, updating external
// bound variable if own width is bigger.
struct AlignedWidthView: ViewModifier {
    @Binding var width: CGFloat

    func body(content: Content) -> some View {
        content
            .background(GeometryReader {
                Color.clear
                    .preference(key: ViewWidthKey.self, value: $0.frame(in: .local).size.width)
            })
            .onPreferenceChange(ViewWidthKey.self) {
                if $0 > self.width {
                    self.width = $0
                }
            }
            .frame(width: width)
    }
}

struct ViewWidthKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}
Asperi
  • 123,447
  • 8
  • 131
  • 245
  • Thanks, I think I see what this is doing, but it will take me some time to understand. I pasted your code into Xcode and it is complaining about the two references to `ViewWidthKey` as not being defined in scope. – zkarj Sep 06 '20 at 07:02
  • 1
    Yes, forgot to add ViewWidthKey from different common module. See updated. – Asperi Sep 06 '20 at 07:30
  • OK, thanks. That goes a long way to solving the matching width problem, but still leaves two unresolved and related issues. The only reason the numeric readouts are the same size is because their content is the same width. If you move a slider to 10.00 then the slider itself shortens to make room for the label. In my real code, these values are always different lengths so I need them all to allow the greatest width. Your method seems like it would allow another variable like `imageWidth` to be used for the Text views, but that won't stop those views from resizing when the content changes. – zkarj Sep 06 '20 at 09:19
  • My previous means of addressing this (when I was originally attempting to use HStacks) was to use a ZStack with a max width Text string in transparent text behind the actual value. I guess this could work in concert with the PreferenceKey approach. – zkarj Sep 06 '20 at 09:20