【问题标题】:Conflicts with custom alignment guides in SwiftUI与 SwiftUI 中的自定义对齐指南冲突
【发布时间】:2020-11-14 18:20:13
【问题描述】:

在我的项目中,我必须使用带有嵌套视图的自定义对齐指南。这很复杂,但我会尝试通过以下方式简化我的案例:

有 3 种可能的视图:一个圆形、一个 upContainer 和一个 downContainer。这两个容器只是其他圆圈/容器的数组。 downContainer 仅将第一个数组与当前行对齐(例如,其他圆圈的位置),而不管第二个数组内部是什么; upContainer 则相反。这是一张图片来可视化它们:

我希望能够使用这三个元素动态构建复杂的视图。因此,我创建了一个具有嵌套关联类型的数据模型(这是一个简单的枚举):

enum Data: Hashable {
    case circle
    case upContainer([Data], [Data])
    case downContainer([Data], [Data])
}

为了手动对齐三个元素,我以这种方式创建了自定义 SwiftUI 对齐方式:

extension VerticalAlignment {
    struct MyAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[VerticalAlignment.center]
        }
    }

    static let myAlignment = VerticalAlignment(MyAlignment.self)
}

我希望特定容器中的圆圈具有相同的颜色,因此我创建了一个扩展程序以快速生成随机颜色:

extension Color {
    static var random: Color {
        return Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
} 

现在我可以创建对应于三个元素的视图:

struct CirclesView: View {

var circles: [Data]

var color: Color = Color.random

var body: some View {
    HStack(alignment: .myAlignment) {
        ForEach(circles, id: \.self) { value in
            switch value {
            case .circle:
                Circle()
                    .frame(width: 20, height: 20)
                    .foregroundColor(color)
            case .upContainer(let firstData, let secondData):
                UpContainerView(container: (firstData, secondData))
            case .downContainer(let firstData, let secondData):
                DownContainerView(container: (firstData, secondData))
            }
        }
    }
}

}

struct UpContainerView: View {

var container: ([Data], [Data])

var color = Color.random

var body: some View {
    VStack(alignment: .leading) {
        CirclesView(circles: container.0, color: color)
        
        CirclesView(circles: container.1, color: color)
            .alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
    }
}

}

struct DownContainerView: View {

var container: ([Data], [Data])

var color = Color.random

var body: some View {
    VStack(alignment: .leading) {
        CirclesView(circles: container.0, color: color)
            .alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
        
        
        CirclesView(circles: container.1, color: color)
    }
}

}

此时问题应该解决了。比如我要渲染这张图:

我应该可以通过写作来做到这一点:

struct ContentView : View {


var myData: [Data] = [.circle, .circle, .downContainer(
                        [.circle, .circle, .circle, .downContainer(
                            [.circle, .circle, .circle, .circle], [.circle]
                        )], [.circle, .circle, .upContainer(
                            [.circle, .circle], [.circle, .circle, .circle, .circle]
                        )]
)]

var body: some View {
    CirclesView(circles: myData)
}

}

然而结果是这样的:

如您所见,第一个容器(棕色容器)并未呈现为 downContainer,实际上它与前两个绿色圆圈居中对齐。这是因为即使我们只为两个数组中的一个设置显式对齐指南,SwiftUI 也会考虑另一个数组的子视图的对齐指南(即使我们想忽略它们)并且存在冲突。

struct DownContainerView: View {

var container: ([Data], [Data])

var color = Color.random

var body: some View {
    VStack(alignment: .leading) {
        CirclesView(circles: container.0, color: color)
            .alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
        
        //We set the explicit alignment guide only for the first array, I am looking for a way to ignore completely the alignment guides of the second one and to calculate the alignment based only on the first one.
        
        CirclesView(circles: container.1, color: color)
    }
}

}

为了解决冲突,我们应该简单地忽略另一个数组的对齐指南。 实际的问题变成了“我们如何忽略那些由 SwiftUI 隐式自动计算的对齐指南?”

注意:我现在可以使用 PreferenceKeys 来解决这个问题,但我认为这个问题应该只使用对齐指南来解决,因为它只是一个对齐问题。我还认为一个解决方案可能是为每个子容器动态创建一个新的自定义对齐指南,但我不知道如何去做(如果在 Swift 中可能的话)。

【问题讨论】:

  • 那篇文章我已经看过了,但是作者不需要处理嵌套视图,因此我的问题不会出现。至于你所说的对齐指南没有被激活,它们实际上是: CircleViews 是父视图,它定义了一个 HStack,对齐类型为“myAlignment”。然后 upContainer 和 downContainer 有两个子视图(我称之为数组),其中只有一个声明了显式指南。感谢您的关注,如果您需要任何进一步的说明,请随时询问。我知道这个问题很复杂,但我真的被困住了。
  • 我想渲染这张图片 - 根据你的逻辑,它不能以常规 .circle 开头,因为顶部有两个蓝色,所以它应该以 .upContainer 开头.我错了吗?我仍然很难理解你的目标和逻辑。
  • 再次感谢您的关注,非常感谢。我的目标是了解自定义对齐指南如何在嵌套视图中相互交互。在我要表示的图像中,最初的两个蓝色圆圈不属于容器,实际上它们仅位于一排圆圈上。相反,红色、黄色和粉红色的显示在两行上,这意味着它们在一个容器中。红色的必须是 downContainer,因为它只将顶部的圆圈与蓝色的圆圈对齐(黄色的圆圈也是如此),而粉色是“向上”的,因为它将底部的圆圈与前面的圆圈(红色)对齐。
  • 好吧,正如您所描述的,我补充说您希望对齐参考线按照添加元素的顺序相互匹配,但事实并非如此。容器提供参考对齐指南。在您的情况下,它是带有 .center 的 HStack,所有内部都通过其引入的(隐式或显式)对齐指南对齐。所以,你的顶部 HStack.center 引入了一条线,所以前 2 个蓝色垂直居中(总是!!),然后你有 VStack 的 VStack,它最终也将居中,实际观察到的(至我明白了)。
  • 没错!问题是“我不希望它被观察到,我该怎么做?”。我想忽略我没有明确设置的对齐指南。对于一层嵌套视图,它只是起作用,因为显式指南似乎比隐式指南具有更高的优先级(只需查看蓝色和灰色容器,它们按照我的意愿对齐),但如果子视图中有显式指南,它也很重要,我想忽略它!我希望我终于设法解释了自己,如果我不清楚,对不起。

标签: swift swiftui alignment


【解决方案1】:

首先提供一些可能有帮助的背景信息:

  1. 每个视图都有一个对齐方式,即视图坐标系中的 x 和 y 值。请注意,这里没有提到名称(如 .top.leading),这些只是告诉视图返回哪个 x 和 y 值作为此布局的对齐指南的方法。
  2. 如果您不告诉视图使用哪种对齐方式,则默认为.center
  3. 父视图 视图通过计算该坐标在其自己的坐标系中的位置来使用子对齐参考线,并放置所有子视图以使对齐全部重合。一些父母可能会忽略 child 值并将 child 放在它想要的位置(例如 HStack 忽略其孩子的 x 值并将孩子并排放置。)

OP 询问是否可以仅通过布局来解决问题,或者是否需要首选项?

答案是:它可以单独使用布局来完成,具体取决于您需要的复杂程度,但您可能需要偏好,具体取决于您需要的复杂程度。

查看 OP 示例,我们看到 CirclesViewHStack 的别名,上下容器都是包含两个 CirclesViews 的 VStack 的别名。

此外,测试用例只有CirclesViews 和Circles 和UpContainerViewDownContainerView(从不上下,这给人一种布局运行良好的错误印象,它甚至不如OP认为的那么好)。

因此,展开测试数据,直接使用HStacks 和VStacks,使用具有固定颜色的Text,这样我们就可以分辨出什么是什么,并将它们全部放在一个视图中,这样我们就可以进行对齐了:

struct SOAlignment: View {
    var body: some View {
        HStack { //CirclesView (solid gray)
            Text("C")
                .foregroundColor(Color.blue)
                .overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
            Text("C")
                .foregroundColor(Color.blue)
                .overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
            VStack(alignment: .leading) { //DownContainerView (black dashes)
                HStack { //CirclesView (top gray dashes)
                    Text("C")
                        .foregroundColor(Color.red)
                    Text("C")
                        .foregroundColor(Color.red)
                    Text("C")
                        .foregroundColor(Color.red)
                    VStack(alignment: .leading) { //DownContainerView (yellow)
                        HStack {
                            Text("C")
                            Text("C")
                            Text("C")
                            Text("C")
                        }
                        HStack {
                            Text("C")
                        }
                    }
                    .foregroundColor(Color.yellow)
                }
                .foregroundColor(Color.red)
                .overlay(Rectangle()
                            .inset(by: 1.0)
                            .strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
                            .foregroundColor(Color.gray))
                HStack { //CirclesView (bottom gray dashes)
                    Text("C")
                        .foregroundColor(Color.red)
                    Text("C")
                        .foregroundColor(Color.red)
                    VStack(alignment: .leading) { //UpContainerView (pink)
                        HStack {
                            Text("C")
                            Text("C")
                        }
                        HStack {
                            Text("C")
                            Text("C")
                            Text("C")
                            Text("C")
                        }
                    }
                    .foregroundColor(Color.lightPink)
                }
                .overlay(Rectangle()
                            .inset(by: 1.0)
                            .strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
                            .foregroundColor(Color.gray))

            }
            .overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
        }
        .border(Color.gray.opacity(0.5))
        .padding()
    }
}

我添加了.leading 水平对齐,因此输出尽可能接近圆形示例,但这些没有任何作用,因此可以忽略。我们只对垂直对齐感兴趣。

我们得到以下输出。

我在顶层视图周围设置了边框。顶部CirclesView 是纯灰色,它包含一个蓝色Circle、一个蓝色Circle 和一个DownContainerView,都带有黑色破折号,DownContainerView 包含两个带有灰色破折号的CirclesView

所有视图都没有对齐,我的意思是每个视图都有恰好一个对齐,它具有默认的.center值。值得在这里暂停一下——我们正在尝试对齐圆圈,它们的中心都有对齐参考线,但蓝色圆圈的中心与其他彩色圆圈的任何中心都不对齐!两个黑色虚线蓝色圆圈的中心当然与黑色虚线DownContainerView/VStack 的中心对齐。

圆圈已被替换为文本的事实暗示了一种可能的解决方案。这与 Apple 使用 .firstTextBaseline.lastTextBaseline 解决的问题完全相同。我们希望我们的前三个红色圆圈与黄色向下容器的顶行对齐,我们希望底部两个红色圆圈与粉红色向上容器的底行对齐。我们可以通过将.firstTextBaseline 放在顶部的灰色虚线CirclesView.lastTextBaseline 放在底部的灰色虚线CirclesView 上来实现这一点。现在它们已经对齐,我们还可以将.firstTextBaseline 放在实心灰色CirclesView 上。

例如

HStack(alignment: .firstTextBaseline) { //CirclesView (solid gray)

我们得到...

万岁问题解决了!不完全是:我们将对齐属性放在 parent CirclesView/HStack 而不是子 Up/DownContainer/VStack 上。通过将实心灰色视图对齐更改为.lastTextBaseline来说明这一点

HStack(alignment: .lastTextBaseline) { //CirclesView (solid gray)

黑色虚线DownContainerView已经变成黑色虚线UpContainerView,本身没有任何变化!

这就是我之前暗示的关于在测试示例数据中的CirclesView 中只有一种类型的容器。我们需要确保 CirclesViews 与 up 和 down 容器都能正常工作,但使用此解决方案,CirclesView(第一个或最后一个文本基线)上的属性将所有直接子容器视图变成一种类型(向上或向下) .

我们这里需要的是自定义对齐,而 OP 的自定义对齐是完美的。所以让我们使用它并将所有文本基线替换为myAlignments。

例如

HStack(alignment: .myAlignment) { //CirclesView (solid gray)

因为myAlignment 默认为.center,所以所有中心对齐方式都与之前相同。没有开发人员的帮助,黑色虚线容器仍然不知道如何在其所有子代的myAlignments 之间进行选择。我们需要明确告诉DownContainerView/VStack 如何计算距离其顶部半圆高的myAlignment 的值,然后给它对齐指南。同样,UpContainerView/VStack 需要一个 .myAlignment 引导在其 .bottom 上方半圆高度。

VStack(alignment: .leading) { //DownContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
    d[.top] + halfCircleHeight
}

VStack(alignment: .leading) { //UpContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
    d[.bottom] - halfCircleHeight
}

现在至关重要的是,CirclesView 指定了它想要与 (alignment: .myAlignment) 的对齐方式,而 Up/DownContainerView 返回与 .alignmentGuide(.myAlignment) 的对齐方式

这是结果:

现在你们中间的鹰眼会注意到,最上面一排的蓝色、红色和黄色圆圈并不完全对齐,同样,底部的红色和粉色圆圈也不完全对齐。这是因为我使用了一个常量

private let halfCircleHeight = 6.0

对于我的计算(默认系统字体高度的一半。)文本在我没有考虑的行上方和下方有一点空白,字体大小的变化也会稍微错位。

这是首选项的来源。如果您有不同大小的 Circles 或 .padding 或其他一些效果,您可能需要获取 Up/DownContainerViews 来询问他们的子视图首选项以获得正确的值来使用在他们的对齐指南计算中。如果需要,孩子将使用anchorPreference 记录其中心,Up/DownContainerView 将在所有孩子视图偏好之间进行选择,并在其对齐指南计算中使用适当的选项。 (这留给读者作为练习。)顺便说一下,anchorPreference 只是一个偏好,它带有关于它定义的坐标系的信息。子级可以在其坐标系(它自己的中心)中设置值,而父级可以在其坐标系(在父视图中的位置)中读取值。

总之,对齐指南没有冲突,因为只有一个值,因此没有任何冲突。希望这可以解释为什么 OP 的两个绿色圆圈 .myAlignment 默认为 .centerVStack(在 DownContainerView 中).myAlignment 对齐,后者也默认为 .center

【讨论】:

    猜你喜欢
    • 2021-09-14
    • 1970-01-01
    • 2021-08-27
    • 2015-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-14
    • 1970-01-01
    相关资源
    最近更新 更多