Sean Rucker
Sean Rucker

Reputation: 1096

Measure the rendered size of a SwiftUI view?

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

Upvotes: 14

Views: 13322

Answers (7)

Saifan Nadaf
Saifan Nadaf

Reputation: 1775

I've created an extension of View with 2 different ways which gives you size (height and width)

Using overlay

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

Using background with 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
}

Usage

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

Upvotes: 0

myself
myself

Reputation: 99

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

}

Upvotes: 4

srgtuszy
srgtuszy

Reputation: 1548

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

Upvotes: 0

Evgeny Karkan
Evgeny Karkan

Reputation: 9612

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.

Upvotes: 1

Jack
Jack

Reputation: 2671

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

Upvotes: 11

Paul B
Paul B

Reputation: 5115

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

Upvotes: 10

Russian
Russian

Reputation: 1306

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

Upvotes: 2

Related Questions