SwiftUI -使用GeometryReader而不修改视图大小

ahy6op9u  于 2023-03-22  发布在  Swift
关注(0)|答案(4)|浏览(212)

我有一个标题视图,它使用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

jv4diomz

jv4diomz1#

回答标题中的问题:

  • 可以将GeometryReader Package 在.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()
    }
}

但是,有一些注意事项/不太明显的行为

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忽略安全区域:

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

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

测量的几何体将可用于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
    }
}

ctehm74n

ctehm74n2#

我尝试了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/

更新

好了,通过你的额外解释,这里你有一个工作解决方案。请注意,预览将不工作,因为safeInsets被报告为零。然而,在模拟器上,它工作正常:
正如你将看到的,我使用视图首选项。它们在任何地方都没有解释,但我目前正在写一篇关于它们的文章,我将很快发布。
它可能看起来太冗长了,但是如果您发现自己使用它太频繁了,您可以将它封装在一个自定义修饰符中。

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

    }
}
d7v8vwbk

d7v8vwbk3#

我设法解决了这个问题,将页面主视图 Package 在GeometryReader中,并将safeAreaInsets传递给MyView。由于它是我们想要整个屏幕的主页面视图,因此可以尽可能贪婪。

q5lcpyga

q5lcpyga4#

我对我最终使用的解决方案并不完全满意,但它确实有效,所以我想我应该分享它。
此示例实现用于在聊天记录中呈现一个条目行的视图。它使用GeometryReader作为上下文视图中最上面的HStack的背景-占据所述视图的整个宽度。
当视图绘制时,chatBalloonMaxWidth状态由GeometryReader Package 的Rectangle()上的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传入初始化器。
如果一个组件的大小调整是通过其他元素完成的(例如,sidebar,draggable),你可以监听NSView.boundsDidChangeNotification,它也会被分派到NotificationCenter.default上。你还可以通过创建一个自定义的NSViewRepresentable并在你的onReceive回调中检查该对象来将范围缩小到单个组件。

相关问题