我与 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)
}
}
}
结果:
我是 SwiftUI 的新手 - 不是 iOS 开发的新手。
我有很多自定义 design/drawing 要做,这在 SwiftUI 中似乎特别困难。
请多多包涵,准备好长篇大论
SwiftUI 的优点在于它基于组合,您可以自动调整大小并适应不同的设备。
但是 让我感到困扰的是,一旦您必须相对于彼此调整事物的大小,您很快就必须求助于 GeometryReader。 而且GeometryReader好像和SwiftUI中的sizing概念很矛盾
与 Autolayout 约束相反,您可以在其中轻松地添加对其他视图或父视图的依赖的相对约束,SwiftUI 似乎并不那么容易做到这一点。
假设我想添加一个视图作为图像的叠加层,确保叠加层始终具有父视图的相对填充 - 比如百分比的边距。
在 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)
}
}
}
结果: