嵌套时(在覆盖层、背景或 ZStack 内)跨堆栈对齐视图

问题描述 投票:0回答:2

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
,我将不得不修改代码的两个地方:

  1. 在前面的
    BucketView
    上添加一个
    HStack
  2. 添加一个与背景中的
    Rectangle
    颜色相匹配的
    HStack
    。 换句话说:它创建了第二个事实来源并且难以维护。

所以,我的意思是,我可以拥有(彩色面板+图标+文本)的所有配置,包括封装在一个视图中的颜色(当然可能有嵌套的子视图),所以代码中只有一个点我需要触摸才能添加或删除“桶”。

ios swiftui alignment overlay swiftui-alignment-guide
2个回答
0
投票

好吧,让我们解决跨堆栈对齐视图的 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
负责自己的布局,不需要了解其他的任何信息。但两者都应该满足您的目的。


0
投票

编辑:改进了对齐位置的估计方式。

以下是我在尝试寻找解决方案时得出的结论的摘要:

  • 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)
}

AlignedOverlays

© www.soinside.com 2019 - 2024. All rights reserved.