衡量SwiftUI视图的渲染大小?

11

在SwiftUI运行其视图渲染阶段后,有没有一种方法来测量视图的计算大小?例如,考虑以下视图:

struct Foo : View {
    var body: some View {
        Text("Hello World!")
            .font(.title)
            .foregroundColor(.white)
            .padding()
            .background(Color.red)
    }
}

选中视图后,计算出的大小将显示在预览画布左下角。有人知道如何在代码中获取该大小吗?

输入图像描述


1
对我来说,SwiftUI 的意图是不关心任何东西的大小。也就是说,除非你需要 - 在这种情况下,你需要声明它。是的,这是一种范式转变。但在这种情况下,为什么你要关心框架大小呢?(我假设你的意思是这样的。我猜想苹果实验室的专家们会问你同样的问题。当你需要声明大小或添加间距时,你有padding()Spacer()Divider()和当然,frame()。因此,在以声明的方式(将其余部分留给适用于所有大小的渲染引擎)表达时,你为什么要关心呢? - user7014451
@dfd 在显示边界内的随机定位是我的 ^^ - Kheldar
这篇由SwiftUI Lab的Javier撰写的三部分博客帮助我解决了这个问题。我强烈推荐有兴趣的人仔细阅读。
  • https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
  • https://swiftui-lab.com/communicating-with-the-view-tree-part-2/
  • https://swiftui-lab.com/communicating-with-the-view-tree-part-3/
- Sean Rucker
例如,当您桥接到UIKit时:UITableView需要大小来配置表视图单元格的高度。@dfd - Jonny
7个回答

10

打印出数值是好的,但能够在父视图(或其他地方)内部使用它们更好。因此,我进一步详细说明了它。

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { (g) -> Path in
            print("width: \(g.size.width), height: \(g.size.height)")
            DispatchQueue.main.async { // avoids warning: 'Modifying state during view update.' Doesn't look very reliable, but works.
                self.rect = g.frame(in: .global)
            }
            return Path() // could be some other dummy view
        }
    }
}

struct ContentView: View {
    @State private var rect1: CGRect = CGRect()
    var body: some View {
        HStack {
            // make to texts equal width, for example
            // this is not a good way to achieve this, just for demo
            Text("Long text").background(Color.blue).background(GeometryGetter(rect: $rect1))
            // You can then use rect in other places of your view:
            Text("text").frame(width: rect1.width, height: rect1.height).background(Color.green)
            Text("text").background(Color.yellow)
        }
    }
}

2
Color.clear 作为一个非常好的虚拟视图,因为它可以扩展到填充所有空间,并且在语法上看起来很不错 :) - aheze

9
您可以使用GeometryReader添加“覆盖层”来查看值。但是在实践中,最好使用“背景”修饰符并离散处理大小值。
struct Foo : View {
    var body: some View {
        Text("Hello World!")
            .font(.title)
            .foregroundColor(.white)
            .padding()
            .background(Color.red)
            .overlay(
                GeometryReader { proxy in
                    Text("\(proxy.size.width) x \(proxy.size.height)")
                }
            )
    }
}

我喜欢这种方式。你能给个提示如何将这个值赋给一个变量或 .frame(height: xx) 吗?我尝试的所有方法都会出现语法错误。 - Enrico
因此,存储值可能不是您想要的,因为SwiftUI是一种声明性语言,状态通常在视图之外进行管理。但是,您可以通过使用首选项将子视图值传递到View层次结构中的父视图来进行通信。基本上,在GeometryReader中,您将传递代理值到PreferenceKey,该键可以由父视图观察。这是一个很好的网站,演示了如何使用SwiftUI首选项 https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/ - Jack
所以在重新阅读@Enrico的评论时,我看到您想使用Geometry Reader的代理创建一个框架。这可能是不必要的,因为GeometryReader将占用其允许的所有空间。但是您可以在GeometryReader的任何子级中使用“proxy”值。 - Jack
哇,谢谢你的帮助,使用GeometryReader后视图本身不会改变,这样好多了! :) - Simon Henn

4

您可以使用 PreferenceKey 来实现这个功能。

struct HeightPreferenceKey : PreferenceKey {
    
    static var defaultValue: CGFloat = 0
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
    
}

struct WidthPreferenceKey : PreferenceKey {
    
    static var defaultValue: CGFloat = 0
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
    
}

struct SizePreferenceKey : PreferenceKey {
    
    static var defaultValue: CGSize = .zero
    
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
    
}

extension View {
    
    func readWidth() -> some View {
        background(GeometryReader {
            Color.clear.preference(key: WidthPreferenceKey.self, value: $0.size.width)
        })
    }
    
    func readHeight() -> some View {
        background(GeometryReader {
            Color.clear.preference(key: HeightPreferenceKey.self, value: $0.size.height)
        })
    }
    
    func onWidthChange(perform action: @escaping (CGFloat) -> Void) -> some View {
        onPreferenceChange(WidthPreferenceKey.self) { width in
            action(width)
        }
    }
    
    func onHeightChange(perform action: @escaping (CGFloat) -> Void) -> some View {
        onPreferenceChange(HeightPreferenceKey.self) { height in
            action(height)
        }
    }
    
    func readSize() -> some View {
        background(GeometryReader {
            Color.clear.preference(key: SizePreferenceKey.self, value: $0.size)
        })
    }
    
    func onSizeChange(perform action: @escaping (CGSize) -> Void) -> some View {
        onPreferenceChange(SizePreferenceKey.self) { size in
            action(size)
        }
    }
    
}

使用方法如下:

struct MyView: View {

    @State private var height: CGFloat = 0

    var body: some View {
        ...
            .readHeight()
            .onHeightChange {
                height = $0
            }
    }

}

2

这是我想出来的一种不太优美的方法来实现这个目标:

最初的回答:

struct GeometryPrintingView: View {

    var body: some View {
        GeometryReader { geometry in
            return self.makeViewAndPrint(geometry: geometry)
        }
    }

    func makeViewAndPrint(geometry: GeometryProxy) -> Text {
        print(geometry.size)
        return Text("")
    }
}

并更新了 Foo 版本:

struct Foo : View {
    var body: some View {
        Text("Hello World!")
            .font(.title)
            .foregroundColor(.white)
            .padding()
            .background(Color.red)
            .overlay(GeometryPrintingView())
    }
}

也许使用 EmptyView()Text("") 更合适。 - rob mayoff
1
@rob mayoff 我也这么认为,但由于某种原因,使用EmptyView()会导致makeViewAndPrint方法不被调用。 - Russian

1

对于想要从Jack的解决方案中获取大小并将其存储在某个属性中以供进一步使用的人:

.overlay(
    GeometryReader { proxy in
        Text(String())
            .onAppear() {
               // Property, eg
               // @State private var viewSizeProperty = CGSize.zero
               viewSizeProperty = proxy.size
            }
            .opacity(.zero)
        }
)

这显然有点粗糙,但如果它能正常工作,为什么不用呢。

0

0
我创建了一个扩展的View,有两种不同的方式可以给你提供大小(高度和宽度)。

使用覆盖层

public extension View {
    /// Determine size of a given view.
    /// - Parameters:
    ///     - callBack: Block called after the size has been measured
    func calculateFrame(_ callBack: @escaping (CGSize) -> Void) -> some View {
        overlay (
            GeometryReader { geo in
                Color.clear.onAppear{ ///Used clear color to create a dummy view
                    callBack(geo.frame(in: .local).size)
                }
            }
        )
    }///Func ends
}///Extension ends

使用PreferenceKey的背景
private struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout Value, nextValue: () -> Value) { }
}

public extension View {
    /// - Parameters:
    ///     - callBack: Block called after the size has been measured
    func calculateFrame2(_ callBack: @escaping (CGSize) -> Void) -> some View {
         background(
             GeometryReader {
                 Color.clear.preference(key: SizePreferenceKey.self,
                                        value: $0.frame(in: .local).size)
             }
         )
        .onPreferenceChange(SizePreferenceKey.self) { callBack($0) }
    }///Func ends
}

使用方法
struct ContentView: View {
    @State private var labelWidth: CGFloat = 0
    @State private var labelHeight: CGFloat = 0

    var body: some View {
        VStack {
            Text("Hello World")
                .calculateFrame {   ///<---- Here you will get size
                    labelWidth = $0.width
                    labelHeight = $0.height
                }
                .padding()
            Text("Text Width - \(labelWidth), \nText Height - \(labelHeight)")
        }
    }
}

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