11

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

Sean Rucker
  • 1,066
  • 2
  • 11
  • 20
  • 1
    To me, the intent of SwiftUI is to not care about the size of anything. That is, unless you do - which in that case, you *declare* it. Yeah, a paradigm shift. But in that context, why do you care what the frame size is? (I'm assuming that what you mean. And I'm guessing that the Apple lab experts would ask you the same thing. You have `padding()`, `Spacer()`, Divider()` and of course, `frame()` when you **need** to declare a size or add spacing. SO speaking *declaritvely* (with leaving the rest up to the rendering engine that works on all sizes) why do you care? –  Jun 14 '19 at 20:54
  • @dfd random positioning within the display bounds is mine ^^ – Kheldar Nov 13 '19 at 12:46
  • This three-part blog post by Javier of SwiftUI Lab helped me solve this problem. I highly recommend reading it in detail for anyone interested. - 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 Jan 14 '20 at 15:08
  • @dfd for example, when you bridging to UIKit: UITableView needs the size to configure height of table view cell. – Jonny Oct 14 '20 at 03:51

6 Answers6

10

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)
        }
    }
}
Paul B
  • 3,989
  • 33
  • 46
  • 2
    `Color.clear` works as a pretty good dummy view, since it expands to fill all space and looks nice syntactically :) – aheze Dec 04 '21 at 02:50
9

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)")
                }
            )
    }
}
Jack
  • 2,503
  • 1
  • 21
  • 15
  • I like this way. Can you give a hint how to get this value into a variable or `.frame(height: xx) `? All the ways i tried gives syntax errors. – Enrico Mar 23 '20 at 09:35
  • So, the storing of the values is probably not what you want since SwiftUI is a declarative language, state is typically managed outside of the view. But you can communicate child-view values up the View heirarchy through the use of Preferences. Essentially, in the GeometryReader, you'd pass the proxy values to a PreferenceKey which could be observed by the parent view. Here's a good site that demonstrates how to use SwiftUI preferences https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/ – Jack Mar 24 '20 at 17:01
  • So in re-reading @Enrico 's comment, I see you want to create a frame using the Geometry Readers's proxy. This may not be necessary, since the GeometryReader will take up as much space as it's allowed. But you can use the `proxy` value in any child of the GeometryReader – Jack Mar 24 '20 at 17:09
  • Omg thanks for that, it's much better since the GeometryReader doesn't change the view itself! :) – Simon Henn Jun 21 '22 at 10:26
4

You can use PreferenceKey to achieve 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)
        }
    }
    
}

Use it like this:

struct MyView: View {

    @State private var height: CGFloat = 0

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

}
myself
  • 79
  • 4
2

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())
    }
}
Russian
  • 1,296
  • 10
  • 15
1

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.

Evgeny Karkan
  • 8,782
  • 2
  • 32
  • 38
0

As others pointed out, GeometryReader and a custom PreferenceKey is the best way forward for now. I've implemented a helper drop-in library which does pretty much that: https://github.com/srgtuszy/measure-size-swiftui

srgtuszy
  • 1,548
  • 1
  • 18
  • 16