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
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
:
alignment: .leading
to keep the overlay left-alignedText
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) }
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)
}
}