Measure the rendered size of a SwiftUI view?

Viewed 8285

Is there a way to measure the computed size of a view after SwiftUI runs its view rendering phase? For example, given the following view:

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

With the view selected, the computed size is displayed In the preview canvas in the bottom left corner. Does anyone know of a way to get access to that size in code?

enter image description here

6 Answers

Printing out values is good, but being able to use them inside the parent view (or elsewhere) is better. So I took one more step to elaborate it.

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

You could add an "overlay" using a GeometryReader to see the values. But in practice it would probably be better to use a "background" modifier and handle the sizing value discretely

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

Here is the ugly way I came up with to achieve this:

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

And updated Foo version:

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

To anyone who wants to obtain a size out of Jack's solution and store it in some property for further use:

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

This is a bit dirty obviously, but why not if it works.

Try to use PreferenceKey like this.

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

}
Related