swiftuiframegeometryreader

How to apply height of GeometryReader frame?


So I use the GeometryReader to read the available width of a container. Then I calculate how large the buttons should be and that decides the true size. However the GeometryReader seem to not be aware of the size of its children.

How should you tell GeometryReader about its true size?

My guess is:

import SwiftUI

struct ContentView: View {
    @State private var containerHeight: Double = 0.0

    var body: some View {
        GeometryReader { proxy in
            Text("Test")
                .task { containerHeight = calculateCustomHeight(using: proxy) }
        }
        .frame(height: containerHeight)
    }
}

This works. However I am not really sure if this is the SwiftUI way or an infinite cycle.

The container height is calculated, which updates @State, which updates the views, which calculates the container height, which updates @State, etc.

I presume it works now because the value is the same, thus does not trigger another update of the view.


Solution

  • I assume you would like to obtain the size value of a view and work with it later. This can be accomplished by utilizing Preferences.

    1. Creating the PreferenceKey

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

    2. Using the PreferenceKey

    Implement the PreferenceKey and make use of GeometryReader on the desired View to extract its size.

                    .background {
                        GeometryReader { geometry in
                            Color.clear.preference(key: SizePreferenceKey.self,
                                                   value: geometry.size)
                        }
                    }
    

    3. Retrieving the size value

            .onPreferenceChange(SizePreferenceKey.self) { size in
                print("size of the red text is: \(size)")
                // use the value however you want
                sizeOfText = size
            }
    

    The entire code then looks like this:

    struct GeometryReaderTest: View {
        
        @State private var sizeOfRedText: CGSize = .zero
        
        var body: some View {
            VStack {
                Text("Green text")
                    .background { Color.green }
                
                Text("Red text")
                    .background { Color.red }
                // PreferenceKey & GeometryReader usage
                    .background {
                        GeometryReader { geometry in
                            Color.clear.preference(key: SizePreferenceKey.self,
                                                   value: geometry.size)
                        }
                    }
            }
            .onPreferenceChange(SizePreferenceKey.self) { size in
                print("size of the red text is: \(size)")
                // use the value however you want
                sizeOfRedText = size
            }
        }
    }
    
    struct SizePreferenceKey: PreferenceKey {
        static var defaultValue: CGSize = .zero
        static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
            value = nextValue()
        }
    }
    
    

    4. Extension

    You can create an extension to conveniently extract the size of different views within your project.

    extension View {
        func getSizeOfView(_ getSize: @escaping ((CGSize) -> Void)) -> some View {
            return self
                .background {
                    GeometryReader { geometry in
                        Color.clear.preference(key: SizePreferenceKey.self,
                                               value: geometry.size)
                        .onPreferenceChange(SizePreferenceKey.self) { value in
                            getSize(value)
                        }
                    }
                }
        }
    }
    

    Then you can call the getSizeOfView like this:

                Text("Red text")
                    .background { Color.red }
                    .getSizeOfView { size in
                        print("size of the red text is: \(size)")
                        sizeOfRedText = size
                    }