A typical problem in SwiftUI is aligning two texts with their baselines in different VStacks
, when the content of these VStacks
differs in height.
Apple has a nice article on how to solve this problem with a custom alignment guide. However, this approach doesn't work when the VStacks
that contain the texts to be aligned are nested inside of an overlay
(or background
) as you can see in the following image.
That makes sense when you assume that overlays and backgrounds do not affect the layout at all which is (to my knowledge) their main purpose. This is the code for the view:
struct SaveOptionsView: View {
var body: some View {
HStack(alignment: .bucket, spacing: 0) { // ← use custom alignment guide
BucketView(imageSystemName: "brain", name: "Remember")
.foregroundColor(.cyan)
BucketView(imageSystemName: "hand.thumbsup.fill", name: "Like")
.foregroundColor(.orange)
}
.ignoresSafeArea()
.frame(width: 300, height: 300)
}
}
struct BucketView: View {
let imageSystemName: String
let name: String
var body: some View {
Rectangle()
.opacity(0.2)
.overlay {
VStack { // ← VStack nested inside of an overlay
Image(systemName: imageSystemName)
.resizable()
.aspectRatio(contentMode: .fit)
Text(name) // ← this is the Text to be aligned
.font(.headline)
.alignmentGuide(.bucket) { context in
context[.firstTextBaseline]
}
}
.padding()
}
}
}
extension VerticalAlignment {
/// A custom alignment for buckets.
private struct BucketAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.bottom]
}
}
static let bucket = VerticalAlignment(
BucketAlignment.self
)
}
Now I was able to solve this alignment problem using a ZStack
instead of an overlay
, but as you can see in the rendered view below, a new problem arises:
The backgrounds (colored Rectangles) are also shifted accordingly, so the exceed the container at the top or bottom.
How do I fix this properly?*
Here's the code for the modified BucketView
using a ZStack
:
struct BucketView: View {
let imageSystemName: String
let name: String
var body: some View {
ZStack {
Rectangle()
.opacity(0.2)
VStack {
Image(systemName: imageSystemName)
.resizable()
.aspectRatio(contentMode: .fit)
Text(name)
.font(.headline)
.alignmentGuide(.bucket) { context in
context[.firstTextBaseline]
}
}
.padding()
}
}
}
* Properly = ?
I'm aware that I could remove the colored Rectangle
from the BucketView
and just put another HStack
with two differently colored Rectangle
s behind the HStack
that contains the BucketView
s.
However, this approach doesn't scale and it doesn't follow proper encapsulation: If I were to add another BucketView
, I would have to modify the code two places:
- add a
BucketView
to theHStack
in the front, - add a
Rectangle
with a matching color to theHStack
in the background. In other words: It creates a second source of truth and is hard to maintain.
So by properly, I mean that I can have all the configuration of (colored panel + icon + text) including the colors encapsulated in one view (that might have nested subviews of course), so there is only a single point in code that I need to touch in order to add or remove a "bucket".