SwiftUI - 如何在不修改视图大小的情况下使用GeometryReader

32

我有一个头部视图,使用edgesIgnoringSafeArea扩展其背景到状态栏下方。为了正确对齐头部视图的内容/子视图,我需要从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


你的两个示例对我来说产生了相同的结果。你能发布一下你的CustomView吗?另外,根据你的屏幕截图,我认为你正在使用Xcode预览。是吗?目前它们并不是非常可靠。 - kontiki
这只是一个带有嵌套的 HStackVStack。我在 Xcode 预览和模拟器中都进行了检查,结果相同。在您的预览提供程序中尝试使用此代码:MyView().previewLayout(.sizeThatFits) - Infinity
1
我正在更新到 beta 3,我的电脑不可用。稍后我会再试并告诉你。 - kontiki
4个回答

19

回答标题中的问题:

  • 可以将GeometryReader包装在.overlay().background()中,这样可以缓解GeometryReader的布局变化效果。 视图将按通常的方式布局,GeometryReader将扩展到视图的完整大小并将geometry发射到其内容构建器闭包中。
  • 还可以设置GeometryReader的框架以停止其扩展的急切性。

例如,此示例通过将GeometryReader包装在覆盖层中,在3/4矩形高度内呈现蓝色矩形和“Hello world”文本(而不是矩形填充所有可用空间):

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

渲染出一个蓝色矩形的三分之四高度上方显示"Hello world"的图像。

然而,有些注意事项/行为并不是非常明显

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

“Hello world” 在安全区域内的渲染效果。

2

.edgesIgnoringSafeArea(.all)应用于背景,会使GeometryReader忽略安全区域:

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

忽略安全区域的“Hello world”渲染图。

通过添加多个覆盖层/背景可以组合出许多布局。

3

测量的几何信息将对GeometryReader的内容可用。而不是父视图或兄弟视图;即使值提取到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
    }
}

运行时警告“在视图更新期间修改状态,这将导致未定义的行为。”


7

我使用了previewLayout并理解你的意思。不过,我认为这是预期的行为。.sizeThatFits的定义是:

当提供运行预览的设备(C)的尺寸时,将容器(A)适应预览(B)的大小。

我插入了一些字母来定义每个部分并使其更加清晰:

A = 预览的最终大小。

B = 您正在使用.previewLayout()修改的内容的大小。在第一种情况下,这是VStack。但在第二种情况下,它是GeometryReader。

C = 设备屏幕的大小。

两个视图的行为不同,因为VStack不贪婪,只获取所需的内容。另一方面,GeometryReader则尝试获取所有内容,因为它不知道其子项需要什么。如果子项要使用较少的内容,则可以这样做,但必须从提供全部开始。

也许如果您编辑问题以准确说明您想要实现什么,我可以进一步完善我的答案。

如果您希望GeometryReader报告VStack的大小,则可以通过将其放入.background修饰符中来实现。但再次强调,我不确定目标是什么,所以也许不行。

我写了一篇关于GeometryReader不同用途的文章。这是链接,以防有所帮助:https://swiftui-lab.com/geometryreader-to-the-rescue/


更新

好的,根据您的额外解释,这里是一个可行的解决方案。请注意,Preview将无法正常工作,因为safeInsets报告为零。但在模拟器上,它可以正常工作:

正如您所看到的,我使用了视图首选项。它们没有任何解释,但我正在撰写一篇关于它们的文章,很快就会发布。

这可能看起来太冗长了,但如果您发现自己经常使用它,可以将其封装在自定义修饰符中。

enter image description here

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 = $0 }

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

    }
}

1
"VStack不是贪心的" 这是一个非常有趣的主题,似乎不同的看法要么是贪心的,要么不是,但我们能控制吗?当我们制作自己的视图时,我们甚至可以控制它吗?我根本找不到任何关于这个的文档,这很奇怪,这是相当基础的。 - Gusutafu
我正在尝试获取safeAreaInset以便为我的文本添加适当的填充。想法是让MyView背景位于状态栏下方,然后使用safeAreaInset来适当地偏移文本。 - Infinity
@Gusutafu 是的,你可以控制你想要使用多少给定空间。甚至你可以超出它。大多数情况下可以使用 .frame() 和 .fixedSize() 来完成。如果你想进一步讨论,可以给我发电子邮件或者提一个新的问题,这样我们就不会把这个话题扯歪了。 - kontiki
2
太棒了,它可以工作。然而,这似乎是解决一个如此简单的问题的一种复杂的方法。 - Infinity
1
我明白了。你说得对。GeometryReader仍然很贪心... :-( 删除解决方案2.. - kontiki
显示剩余2条评论

6

我成功地解决了这个问题,方法是将页面主视图包装在GeometryReader中,并将safeAreaInsets传递到MyView。由于这是我们想要整个屏幕的主页面视图,因此尽可能贪婪是可以的。


1

我对我最终使用的解决方案并不完全满意,但它确实有效,所以我想分享一下。

这个示例实现是为了在聊天记录中呈现一个条目行的视图。它使用GeometryReader作为上层HStack在上下文视图中的背景 - 占据该视图的全部宽度。

当视图绘制时,chatBalloonMaxWidth状态由包装Rectangle()GeometryReader上的onAppear回调设置。这立即应用了所需宽度的三分之一的总宽度。

为了处理窗口调整大小事件(这是一个macOS应用程序),另一个回调被添加到同一个Rectangle() - 这次监听NSWindow上的didResizeNotification。因为SwiftUI的WindowNSWindow的扩展,所以我们仍然可以使用此通知。

setChatBalloonMaxWidth函数中,条件检查新更新的宽度是否真正必要。这样做是为了避免不必要的状态更新 - 特别是那些由当前窗口未分派的应用程序窗口调整大小事件触发的状态更新。

@State private var chatBalloonMaxWidth: CGFloat? = nil;

var body: some View {
    HStack(spacing: 0) {
        messageBalloonSpacer(.leading);
        
        participantImage(.leading)
        
        Text(message)
            .padding(.horizontal, 10)
            .padding(.vertical, 7)
            .padding(isSent ? .trailing : .leading, 8)
            .frame(minHeight: 27)
            .background(messageBackground())
            .foregroundStyle(textColor())
        
        participantImage(.trailing);
        
        messageBalloonSpacer(.trailing);
    }.background(GeometryReader{ geo in
        Rectangle()
            .foregroundColor(.clear)
            .onAppear(perform: { self.setChatBalloonMaxWidth(geo) })
            .onReceive(
                NotificationCenter.default.publisher(for: NSWindow.didResizeNotification),
                perform: { _ in self.setChatBalloonMaxWidth(geo) });
    });
}

private func setChatBalloonMaxWidth(_ geo: GeometryProxy) {
    DispatchQueue.main.async {
        let maxWidthUpdate = geo.size.width / 3;
    
        if maxWidthUpdate == self.chatBalloonMaxWidth { return }

        self.chatBalloonMaxWidth = maxWidthUpdate;
    }
}

虽然我不喜欢这种方法,但它确实有效,并且具有流畅的响应性。它允许我使视图组件匿名化,无需每次使用时将GeometryProxy传递到初始化程序中。

在一个组件中,如果调整大小是由其他元素(例如侧边栏、可拖动元素)完成的,你可以监听NSView.boundsDidChangeNotification,这也会在NotificationCenter.default上分发。进一步可能通过创建自定义的NSViewRepresentable并在onReceive回调中检查该对象来缩小此范围到单个组件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接