McGuire
McGuire

Reputation: 1260

Make parent view's width the width of the smallest child?

How can I make a parent view's width the width of the smallest child?

I can learn the width of the child view using the answer to this question like so:

struct WidthPreferenceKey: PreferenceKey {
    static var defaultValue = CGFloat(0)

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }

    typealias Value = CGFloat
}

struct TextGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
        }
    }
}

struct Content: View {

    @State var width1: CGFloat = 0
    @State var width2: CGFloat = 0

    var body: some View {
        VStack {
            Text("Short")
              .background(TextGeometry())
              .onPreferenceChange(WidthPreferenceKey.self, perform: { self.width1 = $0 })
            Text("Loooooooooooong")
              .background(TextGeometry())
              .onPreferenceChange(WidthPreferenceKey.self, perform: { self.width2 = $0 })
        }
        .frame(width: min(self.width1, self.width2)) // This is always 0
    }

}

But the VStack has always width 0. It's like as if the line .frame(width: min(self.width1, self.width2)) is being called before the widths are set.

Any ideas?

Upvotes: 0

Views: 847

Answers (1)

John M.
John M.

Reputation: 9473

This is a logic bug, in that the system is technically doing exactly what you've told it to, but that result is not what you, as the programmer, intended.

Basically, VStack wants to shrink as much as possible to fit the size of its content. Likewise, Text views are willing to shrink as much as possible (truncating their contents) to fit inside their parent view. The only hard requirement you've given is that the initial frame of the VStack have width of 0. So the following sequence happens:

  1. width1 and width2 are initialized to 0
  2. The VStack sets its width to min(0, 0)
  3. The Text views inside shrink to width 0
  4. WidthPreferenceKey.self is set to 0
  5. .onPreferenceChange sets width1 and width2, which are already 0

All the constraints are satisfied, and SwiftUI happily stops layout.

Let's modify your code with the following:

  1. Make WidthPreferenceKey.Value a typealias to [CGFloat] instead of CGFloat. This way, you can set as many preference key setters as you want, and they will just keep accumulating into an array.
  2. Use one .onPreferenceChange call, which will find the minimum of all the read values, and set the single @State var width: CGFloat property
  3. Add .fixedSize() to the Text views.

Like so:

struct WidthPreferenceKey: PreferenceKey {
    static var defaultValue = [CGFloat]()

    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }

    typealias Value = [CGFloat]
}

struct TextGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle()
                .fill(Color.clear)
                .preference(key: WidthPreferenceKey.self,
                            value: [geometry.size.width])
        }
    }
}

struct Content: View {

    @State var width: CGFloat = 0

    var body: some View {
        VStack {
            Text("Short")
                .fixedSize()
                .background(TextGeometry())
            Text("Loooooooooooong")
                .fixedSize()
                .background(TextGeometry())
        }
        .frame(width: self.width)
        .clipped()
        .background(Color.red.opacity(0.5))
        .onPreferenceChange(WidthPreferenceKey.self) { preferences in
            print(preferences)
            self.width = preferences.min() ?? 0
        }
    }

}

Now the following happens:

  1. width is initialized to 0
  2. The VStack sets its width to 0
  3. The Text views expand outside the VStack, since we've given permission with .fixedSize()
  4. WidthPreferenceKey.self is set to [42.5, 144.5] (or something close)
  5. .onPreferenceChange sets width to 42.5
  6. The VStack sets its width to 42.5 to satisfy its .frame() modifier.

All constraints are satisfied, and SwiftUI stops layout. Note that .clipped() is keeping the edges of the long Text view from displaying, even though they are technically outside the bounds of the VStack.

Upvotes: 2

Related Questions