Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to have a SwiftUI Shape View conform to dimensions of a neighboring view

Tags:

swift

swiftui

Simple Explanation:

I am displayed a SwiftUI Capsule() to display a status color next to a Text() view. In all cases so far the capsule is used inside an HStack with the Text view. I would like to have the height of the Capsule to match the height of the text. This has not been a problem in some situations, such as in a List(). In other cases, however, the capsule view will take up more vertical space than the text.

Additional Complexities

I have wrapped the Capsule() view into a custom view called StatusCapsule() to allow me to take a color and width parameter to adjust more easily. I have tested with just the Capsule() view and experienced the same outcomes. In my problematic view, the HStack is placed within a GridRow(), within a Grid(), within a ScrollView(), within a NavigationStack(). My confusion also comes from that my first and second GridRow() views both had this issue, however, after placing the Grid() in a ScrollView(), the top GridRow() output StatusCapsule() views to my expectation, but the second GridRow() did not.

Below is the code for the StatusCapsule View and a simplified AnalyticsView() view, as well as a screenshot of the results of the code

StatusCapsule() View:

struct StatusCapsule: View {
    
    var color: Color = .white
    var width: CGFloat = 4
    
    var body: some View {
        Capsule()
            .fill(color)
            .frame(width: width)
    }
}

Problematic view with grid and scroll views:

I replaced all dynamic code in the Text views and color parameter fields with static values to simply the code. I did test this simplified code and received the same result as my dynamic view, so to my understanding the two are fundamentally the same.

struct AnalyticsView: View {
    
    @State private var path = NavigationPath()
    @State private var nums: [String] = ["5","9","4","7","33","37","81"]

    var body: some View {
        NavigationStack(path: $path) {
            ScrollView {
                Grid(alignment: .center, horizontalSpacing: 5, verticalSpacing: 5) {
                    GridRow(alignment: .center) {
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .foregroundStyle(Color(.secondarySystemBackground))
                                .padding(2)
                            VStack {
                                HStack {
                                    StatusCapsule(color: .cyan)
                                    StatusCapsule(color: .cyan)
                                    Text("Total Expected: $1,350.65")
                                        .font(.headline)
                                }
                                HStack {
                                    StatusCapsule(color: .cyan)
                                    Text("Total Completed: $25.44")
                                        .font(.subheadline)
                                    Text("|")
                                        .bold()
                                    StatusCapsule(color: .blue)
                                    Text("Total Invoiced: 45.67")
                                        .font(.subheadline)
                                }
                            }
                            .padding()
                        }.gridCellColumns(2)
                    } // Headline Cell
                    GridRow() {
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .foregroundStyle(Color(.secondarySystemBackground))
                                .padding(2)
                            VStack(alignment: .leading) {
                                Text("In Progress: 5").font(.headline)
                                ForEach(nums.prefix(5), id: \.self) { num in
                                    HStack {
                                        StatusCapsule(color: .red)
                                        Text("\(num)")
                                    }
                                }
                            }
                            .padding(.vertical)
                            .scaledToFit()
                        }.gridCellColumns(1)
                        ZStack {
                            RoundedRectangle(cornerRadius: 20)
                                .foregroundStyle(Color(.secondarySystemBackground))
                                .padding(2)
                        }
                    }
                }
            }
            .navigationTitle("Analytics")
        }
    }
}

I have been looking into controlling the frame of views and have not found a solution I'm satisfied with other than delving deep into UI code from before SwiftUI. I would like the capsule view to dynamically conform to the height of the text view in its associated HStack.

I'm expecting the result I see in the first row of the grid, and I'm also confused as to why I'm seeing different results between the two rows once placed in the ScrollView. In regards to this, the capsules in the top GridRow acted exactly as they do in the second GridRow when the entire Grid was not placed in the ScrollView

like image 664
tishly Avatar asked Aug 31 '25 20:08

tishly


1 Answers

Explanation

The reason why the capsule would sometimes take up more vertical space than the text is because a Capsule, like all shapes, is greedy. It will normally use as much space as possible.

Forcing ideal size as a way to fix

One way to fix is to force the containers to adopt their ideal vertical size by applying a .fixedSize modifier, as explained in another answer by Sweeper. Putting the content inside a (vertical) ScrollView has the same effect, see also How to use ideal size for HStack and VStack layout, instead of max size.

However, setting .fixedSize might have side effects on other content. In particular, if you have any Spacer, Divider, other shapes or colors then these might not expand as you would like them to. The RoundedRectangle in the cells of your Grid are cases in point.

Using an overlay to fix

Another way to fix is to apply the StatusCapsule as an overlay to the Text:

  • an overlay is automatically constrained by the size of the content it is applied to (btw, the same applies for a background layer too)
  • use alignment: .leading to keep the overlay left-aligned
  • add leading padding to the Text to make space for the overlay; the size of the padding should corespond to the width of the bar + spacing (previously, the spacing was provided by the HStack).
Text("The quick brown fox\njumps over the lazy dog")
    .padding(.leading, 12)
    .overlay(alignment: .leading) { StatusCapsule(color: .cyan) }

Screenshot

More concise code

As the example above illustrates, if you just want to add a status bar to a single Text item then an HStack is no longer needed to combine them. You could also consider adding a View extension to apply the modifiers:

private extension View {
    func statusCapsule(color: Color) -> some View {
        self
            .padding(.leading, 12)
            .overlay(alignment: .leading) { StatusCapsule(color: color) }
    }
}

This makes it possible to apply a status capsule in a concise way to any text (or in fact, to any view), with the added benefit that the spacing will always be consistent. Here is how the top VStack of your example could be adapted to use this approach:

VStack {
    Text("Total Expected: $1,350.65")
        .font(.headline)
        .statusCapsule(color: .cyan)
        .statusCapsule(color: .cyan)
    HStack {
        Text("Total Completed: $25.44")
            .font(.subheadline)
            .statusCapsule(color: .cyan)
        Text("|")
            .bold()
        Text("Total Invoiced: 45.67")
            .font(.subheadline)
            .statusCapsule(color: .blue)
    }
}

Screenshot

like image 182
Benzy Neez Avatar answered Sep 03 '25 15:09

Benzy Neez