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