user1636130
user1636130

Reputation: 1683

How to make the space between views grow when given a large frame, but be as small as possible otherwise?

This question is essentially about how to define layout behaviour for a SwiftUI View such that it grows/shrinks in a particular way when given different frames externally. IE imagine you are creating a View which will be packaged up in a library and given to somebody else, without you knowing how much space they will give to your view.

The layout I would like to create will contain two horizontal views, indicated by A & B in my diagrams. I would like to control how this view expands if you specify a frame like follows:

Diagram 1: How I'd like my View to look without a frame specified.

// MyView()

|       [A B]       | 

Diagram 2: How I'd like my View to look with a large frame.

// MyView().frame(maxWidth: .infinity)

|[A               B]| 

Diagram Key:


My naive attempts:

Unmodified HStack The behaviour of an unmodified HStack matches Diagram 1 with an unspecified frame successfully, however when given a large frame it's default behaviour is to grow as follows:

// HStack{A B}.frame(maxWidth: .infinity)
|[        AB       ]| 

HStack with a Spacer between the views If I use a Stack with but add a spacer in between the views, the spacer grows to take up the most space possible, regardless of what frame is given. IE I end up with a view that looks like Diagram 2 even when no frame is specified.

// HStack{A Spacer B}

|[A               B]| 

I've been trying to figure out a way to tell a Spacer to prefer to be as small as possible, but to no avail. What other options do we have to achieve this layout?


Edit: To help out, here's some code as a starting point:

struct ContentView: View {
    
    @State var largeFrame: Bool = false
    
    var body: some View {
        VStack{
            
            Toggle("Large Frame", isOn: $largeFrame)
            
            HStack {
                Text("A")
                    .border(Color.red, width: 1)
                
                Text("B")
                    .border(Color.red, width: 1)
            }
            .padding()
            .frame(maxWidth: largeFrame ? .infinity : nil)
            .border(Color.blue, width: 1)
        }
    }
}

Upvotes: 0

Views: 784

Answers (2)

Asperi
Asperi

Reputation: 257709

If MyView is your component and you have control over its content, then a possible approach is to "override" .frame modifiers (all of them, below is one for demo) and compare explicitly outer width provided by frame and inner width of content subviews.

Tested with Xcode 13.4 / iOS 15.5

demo

Main parts:

struct MyView: View {         // << your component
    var outerWidth: CGFloat?  // << injected width !!

    @State private var myWidth = CGFloat.zero // << own calculated !!
        // ...

"overridden" frame modifier to store externally provided parameter

@inlinable public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View {
    var newview = self
    newview.outerWidth = maxWidth   // << inject frame width !!

    return VStack { newview }   // << container to avoid cycling !!
       .frame(minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth, minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight, alignment: alignment)
}

and conditionally activated space depending on width diffs

SubViewA()
.background(GeometryReader {
    Color.clear.preference(key: ViewSideLengthKey.self,
        value: $0.frame(in: .local).size.width)
})

if let width = outerWidth, width > myWidth {    // << here !!
    Spacer()
}

SubViewB()
.background(GeometryReader {
    Color.clear.preference(key: ViewSideLengthKey.self,
        value: $0.frame(in: .local).size.width)
})

Test module is here

Upvotes: 1

Khoi Nguyen
Khoi Nguyen

Reputation: 421

I'm a little confused to what you are saying. Are you asking how to generate space between A and B without forcing the HStack to be window width? If so, if you place a frame on the HStack, then the spacer shoulder only separate the contents to as far as the user desires?

struct ContentView: View {
    var body: some View {
        HStack() {
            Text("A")
            
            Spacer()
            
            Text("B")
        }
        .frame(width: 100)
    }
}

EDIT:

Does the following code work? The HStack(spacing: 0) ensures that the contents the HStack have no spacing between the items and so the "smallest" possible.

struct ContentView: View {
    @State private var customSpacing = true
    @State private var customFrame = CGFloat(100)
    
    var body: some View {
        VStack {
            Button {
                customSpacing.toggle()
            } label: {
                Text("Custom or Not")
            }

            if !customSpacing {
                HStack(spacing: 0) {
                    Text("A")
                    
                    Text("B")
                }
            } else {
                HStack(spacing: 0) {
                    Text("A")
                    
                    Spacer()
                    
                    Text("B")
                }
                .frame(width: customFrame)
            }
        }
    }
}

Upvotes: 1

Related Questions