我与 GeometryReader 的 love/hate 关系

My love/hate relationship with GeometryReader

我是 SwiftUI 的新手 - 不是 iOS 开发的新手。 我有很多自定义 design/drawing 要做,这在 SwiftUI 中似乎特别困难。
请多多包涵,准备好长篇大论

SwiftUI 的优点在于它基于组合,您可以自动调整大小并适应不同的设备。

但是 让我感到困扰的是,一旦您必须相对于彼此调整事物的大小,您很快就必须求助于 GeometryReader。 而且GeometryReader好像和SwiftUI中的sizing概念很矛盾

与 Autolayout 约束相反,您可以在其中轻松地添加对其他视图或父视图的依赖的相对约束,SwiftUI 似乎并不那么容易做到这一点。

假设我想添加一个视图作为图像的叠加层,确保叠加层始终具有父视图的相对填充 - 比如百分比的边距。 topY、bottomY、leadingX 和 trailingX 是相对于超级视图调整大小的方式,因此内容视图将始终适合“框架”。 (这是一个简化版本 - 考虑“框架”不仅仅是盒子。)

在 Autolayout 中,我将只添加概览,然后使用一个因子将叠加层的宽度限制为超级视图的宽度。以这种方式,叠加层的填充将随着父视图的大小调整而跟随。

在 SwiftUI 中执行此操作将使您将基本视图包装在 GeometryReader 中(它本身会导致整个视图填满整个屏幕并更改坐标系)。 目前的问题是,在 SwiftUI 中添加叠加层时,其工作方式与在 UIKit 中在顶部添加视图的方式相同。 现在有两种不同的方法可以使叠加层相对。两者都涉及有限的像素坐标——这是我在这里的主要抱怨。 一旦进入 GeometryReader 的世界,您就进入了基于固定像素定位的世界,并且失去了 SwiftUI 中自动拟合的所有优点。

因此,要使覆盖填充相对于超级视图,我可以:
A) 使用 EdgeInsets() 添加 .padding,其中边缘插入由超级视图大小的一个因子计算。
B) 使用 .offset() 和 .frame() 通过计算出的相对于父视图大小的因子来偏移叠加层的位置和大小。

仅使用矩形时,这非常简单:

struct RelativeView: View {
    var body: some View {
        GeometryReader { geo in
        Rectangle() // Superview
            .foregroundColor(Color.blue)
            .overlay(Rectangle() // Relative overview (content view)
                        .foregroundColor(Color.yellow)
                        .padding(EdgeInsets(top: geo.size.height/4, leading: (geo.size.width*0.25)/2, bottom: geo.size.height/4, trailing: (geo.size.width*0.25)/2))
                        .overlay(Text("Yellow overlay must be 1/2 height and 3/4 width of the blue"))
            )
        }
    }
}

当超级视图是缩放到 .fit 的图像时,真正的问题就开始了:

struct RelativeView: View {
    var body: some View {
        GeometryReader { geo in
            
            let realImageWidth = CGFloat(4032)
            let viewWidth = geo.size.width
            let relativeWidthFactor = viewWidth/realImageWidth
            let leadingX = CGFloat(1110)
            let trailingX = CGFloat(750)
            let topY = CGFloat(770)
            let bottomY = CGFloat(936)
            
            Image("TestBackground")
                .resizable()
                .aspectRatio(contentMode: .fit) // Superview
                .overlay(Rectangle() // Relative overview (content view)
                        .foregroundColor(Color.yellow)
                        .padding(EdgeInsets(top: topY * relativeWidthFactor, leading: leadingX * relativeWidthFactor, bottom: bottomY * relativeWidthFactor, trailing: trailingX * relativeWidthFactor))
                        .overlay(Text("Yellow overlay must be 1/2 height and 3/4 width of the blue"))
                )
        }
    }
}

请注意,未正确居中的居中文本是由于 Rectangle 不遵守填充引起的。如果我将黄色矩形包裹在一个单独的视图中,它将位于黄色框而不是超级框的中心。 (不要让我开始说......)

当我在另一个视图中使用此视图时,问题开始浮出水面。然后 GeometryReader 开始扰乱动态的 SwiftUI 自动世界。

import SwiftUI

struct GrainCart3dView: View {
    var body: some View {
        GeometryReader { geo in
            VStack {
                Rectangle().foregroundColor(.green)
                RelativeContainerView()
            }
        }
    }
}


struct RelativeContainerView: View {
    var body: some View {
        GeometryReader { geo in
            
            let realImageWidth = CGFloat(4032)
            let viewWidth = geo.size.width
            let relativeWidthFactor = viewWidth/realImageWidth
            let leadingX = CGFloat(1110)
            let trailingX = CGFloat(750)
            let topY = CGFloat(770)
            let bottomY = CGFloat(936)
            
            Image("TestBackground")
                .resizable()
                .aspectRatio(contentMode: .fit) // Superview
                .overlay(RelativeContentView() // Relative overview (content view)
                        .padding(EdgeInsets(top: topY * relativeWidthFactor, leading: leadingX * relativeWidthFactor, bottom: bottomY * relativeWidthFactor, trailing: trailingX * relativeWidthFactor))
                )
        }
    }
}

struct RelativeContentView: View {
    var body: some View {
        Rectangle()
            .foregroundColor(.yellow)
            .overlay(Text("Yellow overlay must be 1/2 height and 3/4 width of the blue"))
    }
}

在 iPad 横向模式下,均匀分布导致框架视图未填满整个宽度,并且由于框架填充是根据宽度计算的,因此现在是错误的。

iPad 上的内容视图现在在哪里?

VStack 均匀分布两个视图,但假设我想在宽度上适应框架视图并保持纵横比,然后将绿色视图适应其余视图。

在 Autolayout 中,我可以设置框架视图的优先级,但这不是它与 SwiftUI 一起工作的方式。 我使用 SwiftUI 的唯一方法是将它包装在另一个超级视图中,并使用另一个 GeomotryReader 在数学上设置绿色视图和框架视图之间的计算关系。更糟糕的是,我必须在两者上都设置尺寸——无法设置一个的尺寸并自动用第二个“填补空白”。

我手头的问题是,使用 GeometryReader 的大小和定位似乎与像素非常相关,与 SwiftUI 的相对和动态概念相去甚远。

所以,为了澄清我正在寻找的是制作一个框架,其中填充相对于超级视图大小缩放,同时垂直分布不均匀。

如何以更好、更通用的方式处理此问题?

首先 - 您可以使用黄色和蓝色视图简化第一部分。您可以只设置 Text 的框架大小,添加背景,然后让 GeometryReader 填充叠加层。这比尝试计算每条边上的插图容易。

代码:

struct ContentView: View {
    var body: some View {
        Color.blue
            .overlay(
                GeometryReader { geo in
                    Text("Yellow overlay must be 1/2 height and 3/4 width of the blue")
                        .frame(width: geo.size.width * 0.75, height: geo.size.height * 0.5)
                        .background(Color.yellow)
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                }
            )
    }
}

接下来,我们做与上面类似的事情,但现在使用图像:

struct RelativeView: View {
    var body: some View {
        Image("TestBackground")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .overlay(
                GeometryReader { geo in
                    Text("Yellow overlay must be 1/2 height and 3/4 width of the blue")
                        .frame(width: geo.size.width * 0.75, height: geo.size.height * 0.5)
                        .background(Color.yellow)
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                }
            )
    }
}

结果:


对于尝试添加另一个视图的下一部分,您只需进行这些小的调整即可。这只是使图像缩放以适合屏幕,并使其优先于绿色。

您可能希望使用 VStack(spacing: 0) { ... } 来消除绿色和图像之间的间隙。

代码:

struct ContentView: View {
    var body: some View {
        VStack {
            Color.green

            RelativeView()
                .scaledToFit()
                .layoutPriority(1)
        }
    }
}

结果: