SwiftUI - 在不修改视图大小的情况下使用 GeometryReader
SwiftUI - Using GeometryReader Without Modifying The View Size
我有一个 header 视图,它使用 edgesIgnoringSafeArea
将其背景扩展到状态栏下方。要正确对齐 header 视图的 content/subviews,我需要 GeometryReader
中的 safeAreaInsets
。但是,当使用 GeometryReader
时,我的视图不再具有合适的尺寸。
不使用的代码 GeometryReader
struct MyView : View {
var body: some View {
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.bottom, 64)
.background(Color.blue)
}
}
预览
代码使用GeometryReader
struct MyView : View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.top, geometry.safeAreaInsets.top)
.padding(.bottom, 64)
.background(Color.blue)
.fixedSize()
}
}
}
预览
有没有办法在不修改底层视图大小的情况下使用 GeometryReader
?
我试过 previewLayout,我明白你的意思了。但是,我认为这种行为符合预期。 .sizeThatFits 的定义是:
Fit the container (A) to the size of the preview (B) when offered the
size of the device (C) on which the preview is running.
我插入了一些字母来定义每个部分并使其更加清晰:
A = 预览的最终大小。
B = 您使用 .previewLayout() 修改的内容的大小。在第一种情况下,它是 VStack。但在第二种情况下,它是 GeometryReader。
C = 设备屏幕的大小。
两种视图的行为不同,因为 VStack 不贪心,只取它需要的东西。另一方面,GeometryReader 试图拥有这一切,因为它不知道它的 child 想要使用什么。 child想少用也可以,但要从什么都给开始
也许如果你编辑你的问题来准确解释你想要完成的事情,我可以稍微改进我的答案。
如果您希望 GeometryReader 报告 VStack 的大小。你可以通过将它放在 .background 修饰符中来实现。但同样,我不确定目标是什么,所以也许这是不行的。
我写了一篇关于 GeometryReader 不同用途的文章。这是 link,以防有帮助:https://swiftui-lab.com/geometryreader-to-the-rescue/
更新
好的,有了你的额外解释,你就有了一个可行的解决方案。请注意,预览将不起作用,因为 safeInsets 被报告为零。然而,在模拟器上,它工作正常:
如您所见,我使用视图首选项。它们在任何地方都没有解释,但我目前正在写一篇关于它们的文章,我很快就会 post。
它可能看起来太冗长,但如果你发现自己经常使用它,你可以将它封装在自定义修饰符中。
import SwiftUI
struct InsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct InsetGetter: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().preference(key: InsetPreferenceKey.self, value: geometry.safeAreaInsets.top)
}
}
}
struct ContentView : View {
var body: some View {
MyView()
}
}
struct MyView : View {
@State private var topInset: CGFloat = 0
var body: some View {
VStack {
CustomView(inset: topInset)
.padding(.horizontal)
.padding(.bottom, 64)
.padding(.top, topInset)
.background(Color.blue)
.background(InsetGetter())
.edgesIgnoringSafeArea(.all)
.onPreferenceChange(InsetPreferenceKey.self) { self.topInset = [=10=] }
Spacer()
}
}
}
struct CustomView: View {
let inset: CGFloat
var body: some View {
VStack {
HStack {
Text("C \(inset)").color(.white).fontWeight(.bold).font(.title)
Spacer()
}
HStack {
Text("A").color(.white)
Text("B").color(.white)
Spacer()
}
}
}
}
我设法通过将页面主视图包装在 GeometryReader
中并将 safeAreaInsets
传递给 MyView
来解决这个问题。因为是主页面视图,我们想要整个屏幕,所以尽可能贪心是可以的。
标题问题的答案:
- 可以将
GeometryReader
包裹在 .overlay()
或 .background()
中。这样做将减轻 GeometryReader 的布局更改效果。视图将正常布局,GeometryReader 将扩展到视图的完整大小并将 geometry
发送到其内容构建器闭包中。
- 也可以设置GeometryReader的frame停止急于展开。
例如,此示例渲染了一个蓝色矩形,并在矩形高度的 3/4 处渲染了一个“Hello world”文本(而不是矩形填满所有可用 space),方法是将叠加层中的 GeometryReader:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
})
Spacer()
}
}
另一个通过在 GeometryReader 上设置框架来实现相同效果的示例:
struct MyView : View {
var body: some View {
GeometryReader { geo in
Rectangle().fill(Color.blue)
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
}
.frame(height: 150)
Spacer()
}
}
但是,有一些注意事项/不是很明显的行为
1
视图修改器适用于应用它们之前的所有内容,而不适用于之后的任何内容。在 .edgesIgnoringSafeArea(.all)
之后添加的叠加层/背景将尊重安全区域(不参与忽略安全区域)。
此代码在安全区域内呈现“Hello world”,而蓝色矩形忽略安全区域:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.edgesIgnoringSafeArea(.all)
.overlay(VStack {
Text("Hello world")
Spacer()
})
Spacer()
}
}
2
将.edgesIgnoringSafeArea(.all)
应用到背景使GeometryReader 忽略 SafeArea:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
VStack {
Text("Hello world")
// No effect, safe area is set to be ignored.
.padding(.top, geo.safeAreaInsets.top)
Spacer()
}
})
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
可以通过添加多个叠加层/背景来组成多个布局。
3
测量的几何图形将可用于 GeometryReader 的内容。不对 parent 或兄弟意见;即使值被提取到 State 或 ObservableObject 中。如果发生这种情况,SwiftUI 将发出运行时警告:
struct MyView : View {
@State private var safeAreaInsets = EdgeInsets()
var body: some View {
Text("Hello world")
.edgesIgnoringSafeArea(.all)
.background(GeometryReader(content: set(geometry:)))
.padding(.top, safeAreaInsets.top)
Spacer()
}
private func set(geometry: GeometryProxy) -> some View {
self.safeAreaInsets = geometry.safeAreaInsets
return Color.blue
}
}
我有一个 header 视图,它使用 edgesIgnoringSafeArea
将其背景扩展到状态栏下方。要正确对齐 header 视图的 content/subviews,我需要 GeometryReader
中的 safeAreaInsets
。但是,当使用 GeometryReader
时,我的视图不再具有合适的尺寸。
不使用的代码 GeometryReader
struct MyView : View {
var body: some View {
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.bottom, 64)
.background(Color.blue)
}
}
预览
代码使用GeometryReader
struct MyView : View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.top, geometry.safeAreaInsets.top)
.padding(.bottom, 64)
.background(Color.blue)
.fixedSize()
}
}
}
预览
有没有办法在不修改底层视图大小的情况下使用 GeometryReader
?
我试过 previewLayout,我明白你的意思了。但是,我认为这种行为符合预期。 .sizeThatFits 的定义是:
Fit the container (A) to the size of the preview (B) when offered the size of the device (C) on which the preview is running.
我插入了一些字母来定义每个部分并使其更加清晰:
A = 预览的最终大小。
B = 您使用 .previewLayout() 修改的内容的大小。在第一种情况下,它是 VStack。但在第二种情况下,它是 GeometryReader。
C = 设备屏幕的大小。
两种视图的行为不同,因为 VStack 不贪心,只取它需要的东西。另一方面,GeometryReader 试图拥有这一切,因为它不知道它的 child 想要使用什么。 child想少用也可以,但要从什么都给开始
也许如果你编辑你的问题来准确解释你想要完成的事情,我可以稍微改进我的答案。
如果您希望 GeometryReader 报告 VStack 的大小。你可以通过将它放在 .background 修饰符中来实现。但同样,我不确定目标是什么,所以也许这是不行的。
我写了一篇关于 GeometryReader 不同用途的文章。这是 link,以防有帮助:https://swiftui-lab.com/geometryreader-to-the-rescue/
更新
好的,有了你的额外解释,你就有了一个可行的解决方案。请注意,预览将不起作用,因为 safeInsets 被报告为零。然而,在模拟器上,它工作正常:
如您所见,我使用视图首选项。它们在任何地方都没有解释,但我目前正在写一篇关于它们的文章,我很快就会 post。
它可能看起来太冗长,但如果你发现自己经常使用它,你可以将它封装在自定义修饰符中。
import SwiftUI
struct InsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct InsetGetter: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().preference(key: InsetPreferenceKey.self, value: geometry.safeAreaInsets.top)
}
}
}
struct ContentView : View {
var body: some View {
MyView()
}
}
struct MyView : View {
@State private var topInset: CGFloat = 0
var body: some View {
VStack {
CustomView(inset: topInset)
.padding(.horizontal)
.padding(.bottom, 64)
.padding(.top, topInset)
.background(Color.blue)
.background(InsetGetter())
.edgesIgnoringSafeArea(.all)
.onPreferenceChange(InsetPreferenceKey.self) { self.topInset = [=10=] }
Spacer()
}
}
}
struct CustomView: View {
let inset: CGFloat
var body: some View {
VStack {
HStack {
Text("C \(inset)").color(.white).fontWeight(.bold).font(.title)
Spacer()
}
HStack {
Text("A").color(.white)
Text("B").color(.white)
Spacer()
}
}
}
}
我设法通过将页面主视图包装在 GeometryReader
中并将 safeAreaInsets
传递给 MyView
来解决这个问题。因为是主页面视图,我们想要整个屏幕,所以尽可能贪心是可以的。
标题问题的答案:
- 可以将
GeometryReader
包裹在.overlay()
或.background()
中。这样做将减轻 GeometryReader 的布局更改效果。视图将正常布局,GeometryReader 将扩展到视图的完整大小并将geometry
发送到其内容构建器闭包中。 - 也可以设置GeometryReader的frame停止急于展开。
例如,此示例渲染了一个蓝色矩形,并在矩形高度的 3/4 处渲染了一个“Hello world”文本(而不是矩形填满所有可用 space),方法是将叠加层中的 GeometryReader:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
})
Spacer()
}
}
另一个通过在 GeometryReader 上设置框架来实现相同效果的示例:
struct MyView : View {
var body: some View {
GeometryReader { geo in
Rectangle().fill(Color.blue)
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
}
.frame(height: 150)
Spacer()
}
}
但是,有一些注意事项/不是很明显的行为
1
视图修改器适用于应用它们之前的所有内容,而不适用于之后的任何内容。在 .edgesIgnoringSafeArea(.all)
之后添加的叠加层/背景将尊重安全区域(不参与忽略安全区域)。
此代码在安全区域内呈现“Hello world”,而蓝色矩形忽略安全区域:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.edgesIgnoringSafeArea(.all)
.overlay(VStack {
Text("Hello world")
Spacer()
})
Spacer()
}
}
2
将.edgesIgnoringSafeArea(.all)
应用到背景使GeometryReader 忽略 SafeArea:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
VStack {
Text("Hello world")
// No effect, safe area is set to be ignored.
.padding(.top, geo.safeAreaInsets.top)
Spacer()
}
})
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
可以通过添加多个叠加层/背景来组成多个布局。
3
测量的几何图形将可用于 GeometryReader 的内容。不对 parent 或兄弟意见;即使值被提取到 State 或 ObservableObject 中。如果发生这种情况,SwiftUI 将发出运行时警告:
struct MyView : View {
@State private var safeAreaInsets = EdgeInsets()
var body: some View {
Text("Hello world")
.edgesIgnoringSafeArea(.all)
.background(GeometryReader(content: set(geometry:)))
.padding(.top, safeAreaInsets.top)
Spacer()
}
private func set(geometry: GeometryProxy) -> some View {
self.safeAreaInsets = geometry.safeAreaInsets
return Color.blue
}
}