Hug subviews in SwiftUI

So I actually found a way to do this. First I tried putting Spacers around the views in various configurations, to try to push it together, but that didn't work. Then I realised I could perhaps use the .background modifier, and that actually did work. It seems to let the owning view calculate its size first, and then just takes that as its frame, which is exactly what I want.

This is just an example with some hacks to get the right height, but that is a small detail, and in my particular use case it is not needed. Probably not here either if you're clever enough.

var body: some View {
    VStack(spacing: 10) {
        Text("Short").background(Color.green)
        Text("A longer text").background(Color.green)
        Text("Dummy").opacity(0)
    }
    .background(backgroundView)
    .background(Color.red)
    .padding()
    .background(Color.blue)
}

var backgroundView: some View {
    VStack(spacing: 10) {
        Spacer()
        Spacer()
        Rectangle().fill(Color.yellow)
    }
}

The blue view and all the color backgrounds are of course just to make it easier to see. This code produces this:

enter image description here


There is no modifier (AFAIK) to accomplish this, so here's my approach. If this is something you are going to use too often, it could be worth creating your own modifier.

Also note that here I am using standard preferences, but anchor preferences are even better. It is a heavy topic to explain here. I've written an article that you can check here: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

You can use the code below to accomplish what you are looking for.

import SwiftUI

struct MyRectPreference: PreferenceKey {
    typealias Value = [CGRect]

    static var defaultValue: [CGRect] = []

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

struct ContentView : View {
    @State private var widestText: CGFloat = 0

    var body: some View {
        VStack {
            Text("Hello").background(RectGetter())
            Text("Wonderful World!").background(RectGetter())
            Rectangle().fill(Color.blue).frame(width: widestText, height: 30)
            }.onPreferenceChange(MyRectPreference.self, perform: { prefs in
                for p in prefs {
                    self.widestText = max(self.widestText, p.size.width)
                }
            })
    }
}

struct RectGetter: View {

    var body: some View {
        GeometryReader { geometry in
            Rectangle()
                .fill(Color.clear)
                .preference(key: MyRectPreference.self, value: [geometry.frame(in: .global)])
        }
    }
}

Tags:

Swift

Swiftui