SwiftUI 中的一个典型问题是,当这些
VStacks
的内容高度不同时,将两个文本与其不同 VStacks
的基线对齐。
Apple 有一篇不错的文章,介绍了如何使用自定义对齐指南解决此问题。但是,当包含要对齐的文本的
VStacks
嵌套在 overlay
(或 background
)内部时,此方法不起作用,如下图所示。
当您假设覆盖层和背景根本不影响布局(据我所知)这是它们的主要目的时,这是有道理的。这是视图的代码:
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
)
}
现在我可以使用
ZStack
而不是 overlay
来解决这个对齐问题,但是正如您在下面的渲染视图中看到的,出现了一个新问题:
背景(彩色矩形)也会相应移动,因此超出顶部或底部的容器。
这是使用
BucketView
修改后的 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()
}
}
}
我知道我可以从
Rectangle
中删除彩色 BucketView
,然后在包含 HStack
的 Rectangle
后面放置另一个带有两个不同颜色 HStack
的 BucketView
。
但是,这种方法无法扩展,并且不遵循正确的封装:如果我要添加另一个
BucketView
,我将不得不修改代码的两个地方:
BucketView
上添加一个HStack
,Rectangle
颜色相匹配的 HStack
。
换句话说:它创建了第二个事实来源并且难以维护。所以,我的意思是,我可以拥有(彩色面板+图标+文本)的所有配置,包括封装在一个视图中的颜色(当然可能有嵌套的子视图),所以代码中只有一个点我需要触摸才能添加或删除“桶”。
好吧,让我们解决跨堆栈对齐视图的 SwiftUI 难题。我们在这里面临的挑战是对齐位于覆盖层或背景中的不同
VStacks
中的文本。苹果对此有一个很好的指南,但这里的问题是我们的 VStacks
嵌套在覆盖层中,使事情变得更加棘手。
使用
ZStack
而不是覆盖的解决方案是一个聪明的解决方法。然而,它确实产生了一个新问题,即背景发生移动,导致顶部或底部溢出。
但是别担心,我们还没有被难住。我建议对您的
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()
}.background(Rectangle().opacity(0.2))
}
}
在此版本中,彩色背景矩形被分配给
ZStack
作为背景,这可以防止它随文本一起移动。由于对文本应用了自定义 .bucket
对齐指南,您所需的文本对齐方式仍然会发生。
通过分离对齐和背景呈现的关注点,您将获得一种更易于维护、遵循正确封装并优雅地处理您的情况的方法。您现在可以轻松添加或删除“存储桶”,只需触摸代码中的一个点即可。
尝试一下,看看它是否达到了所需的布局。
就我个人而言,我的重点是创建一个健壮、可维护且不依赖于任何“幻数”的解决方案。我认为这是一种不同的方法,可以实现这一目标。
我们可以使用
GeometryReader
来计算每个 BucketView
中叠加层的正确对齐方式:
struct SaveOptionsView: View {
var body: some View {
HStack(spacing: 0) {
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
@State private var textHeight: CGFloat = .zero
var body: some View {
ZStack(alignment: .bottom) {
Rectangle()
.opacity(0.2)
VStack {
Spacer()
Image(systemName: imageSystemName)
.resizable()
.aspectRatio(contentMode: .fit)
Text(name)
.font(.headline)
.alignmentGuide(.bottom) { context in
context[.bottom]
}
.background(GeometryGetter(bindTo: $textHeight))
Spacer()
.frame(height: textHeight)
}
.padding()
}
}
}
struct GeometryGetter: View {
@Binding var toSave: CGFloat
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: HeightPreferenceKey.self, value: geometry.size.height)
}
.onPreferenceChange(HeightPreferenceKey.self) { value in
toSave = value
}
}
}
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
在此解决方案中,
GeometryGetter
视图将文本的高度保存到@State
变量。然后在 Spacer
中使用该高度,强制文本沿其底部边缘对齐。 ZStack
设置为沿底部对齐其子项,Spacer
中的顶部 VStack
将其他所有内容向下推动。
该解决方案的优点是完全动态:无论文本或其他元素的大小如何变化,基线都会保持对齐。此外,它不会破坏
BucketView
的封装 - 每个 BucketView
负责自己的布局,不需要了解其他的任何信息。但两者都应该满足您的目的。
编辑:改进了对齐位置的估计方式。
以下是我在尝试寻找解决方案时得出的结论的摘要:
HStack
内使用对齐参考线不起作用,这就是导致第二个屏幕截图中背景偏移的原因HStack
使用默认的垂直对齐方式我找不到让叠加层在自然位置对齐的方法。但考虑到对齐仅用于桶,可以非常可靠地估计对齐位置,如下所示:
struct SaveOptionsView: View {
var body: some View {
HStack(spacing: 0) { // No alignment needed here
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(alignment: .centerBucket) { // Align the 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[.bottom]
}
}
.padding()
}
}
}
extension VerticalAlignment {
/// A custom alignment for buckets.
private struct BucketAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
// This alignment is only used for buckets and is based
// on a knowledge of the usual content. The images are
// usually quite square and they are scaled to fit the
// space available. So use the smallest dimension as an
// estimate of the typical image height
let approxImageHeight = min(context.width, context.height)
// Add a small amount for the label. This is a better
// approach than aligning to the text baseline, for the
// case of when the frame is wider than it is tall
let approxOverlayHeight = approxImageHeight + 12
// Compute the position for guidelines to align to
let pos = context[VerticalAlignment.center] + approxOverlayHeight / 2
// Don't go over the bottom edge
return min(pos, context[.bottom])
}
}
static let bucket = VerticalAlignment(
BucketAlignment.self
)
}
extension Alignment {
static let centerBucket = Alignment(horizontal: .center, vertical: .bucket)
}