51

I need to get width of a rendered view in SwiftUI, which is apparently not that easy.

The way I see it is that I need a function that returns a view's dimensions, simple as that.

var body: some View {
    VStack(alignment: .leading) {
        Text(timer.name)
            .font(.largeTitle)
            .fontWeight(.heavy)
        Text(timer.time)
            .font(.largeTitle)
            .fontWeight(.heavy)
            .opacity(0.5)
    }
}
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Alexey Primechaev
  • 907
  • 1
  • 6
  • 13

4 Answers4

82

The only available mechanism to get the dimension of a view, that is auto-resized by SwiftUI, is the GeometryReader. The GeometryReader is a proxy view that returns the dimensions of the container in which your view gets rendered.

struct SomeView: View {
    
    @State var size: CGSize = .zero
    
    var body: some View {
        VStack {
            Text("VStack width: \(size.width)")
            Text("VStack height: \(size.height)")
            
            GeometryReader { proxy in
                HStack {} // just an empty container to triggers the onAppear
                    .onAppear {
                        size = proxy.size
                    }
            }
        }
        
    }
}

The printed size is the dimension of the VStack.

How to get the dimension of a View

Now that we know that the GeometryReader gives us the size of the container, the usual follow-up question is: how do I use it to get the size of a specific view?

To do this we need to move the geometry reader one level below our targeted view. How? We could add an empty background that gets the size of the targeted view and sends this information back to a Binding.

Let's create a SizeCalculator ViewModifier so that we can use this functionality on every view:

struct SizeCalculator: ViewModifier {
    
    @Binding var size: CGSize
    
    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { proxy in
                    Color.clear // we just want the reader to get triggered, so let's use an empty color
                        .onAppear {
                            size = proxy.size
                        }
                }
            )
    }
}

extension View {
    func saveSize(in size: Binding<CGSize>) -> some View {
        modifier(SizeCalculator(size: size))
    }
}

The job of the SizeCalculator is to add a GeometryReader as the background of our target view. On appear, so after SwiftUI has rendered the content, it will send the size back to the Binding.

Usage:

struct SomeView: View {
    
    @State var size: CGSize = .zero
    
    var body: some View {
        VStack {
            Text("text width: \(size.width)")
            Text("text height: \(size.height)")
            
            Text("hello")
                .saveSize(in: $size)
        }
        
    }
}

how to get the size of a view in swiftUI

Giuseppe Sapienza
  • 4,101
  • 22
  • 23
  • 1
    If I am not to calculate dimensions, then how would I go about implementing a layout similar to this https://www.dropbox.com/s/p4m47u3fhup7o8j/2.png?dl=0 – Alexey Primechaev Aug 20 '19 at 16:27
  • 1
    I mean, I need to know the width of a cell to be able to calculate the numbers of cells in a row. – Alexey Primechaev Aug 20 '19 at 16:28
  • @MaxPower mmh I suggest you to open a new question with a title like that "How to calculate number of cells in a Row" anyway try to look at this https://github.com/Q-Mobile/QGrid – Giuseppe Sapienza Aug 21 '19 at 09:05
  • 3
    @GiuseppeSapienza thanks for this nice solution. Do you know why the call of ```DispatchQueue.main.async {...}```is necessary in order to work? I would have thought that ```makeView()``` is running on the main thread already. – Lindemann Aug 31 '21 at 06:23
  • @Lindemann no it's not necessary anymore. I have updated the answer with a better approach. – Giuseppe Sapienza Feb 26 '23 at 10:55
44

Getting the dimensions of a child view is the first part of the task. Bubbling the value of dimensions up is the second part. GeometryReader gets the dims of the parent view which is probably not what you want. To get the dims of the child view in question we might call a modifier on its child view which has actual size such as .background() or .overlay()

struct GeometryGetterMod: ViewModifier {
    
    @Binding var rect: CGRect
    
    func body(content: Content) -> some View {
        print(content)
        return GeometryReader { (g) -> Color in // (g) -> Content in - is what it could be, but it doesn't work
            DispatchQueue.main.async { // to avoid warning
                self.rect = g.frame(in: .global)
            }
            return Color.clear // return content - doesn't work
        }
    }
}

struct ContentView: View {
    @State private var rect1 = CGRect()
    var body: some View {
        let t = HStack {
            // make two texts equal width, for example
            // this is not a good way to achieve this, just for demo
            Text("Long text").overlay(Color.clear.modifier(GeometryGetterMod(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)
        }
        print(rect1)
        return t
    }
}

Here is another convenient way to get and do something with the size of current view: readSize function.

extension View {
  func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
    background(
      GeometryReader { geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      }
    )
    .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
  }
}

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

Usage:

struct ContentView: View {
    @State private var commonSize = CGSize()
    var body: some View {
        VStack {
        Text("Hello, world!")
            .padding()
            .border(.yellow, width: 1)
            .readSize { textSize in
                commonSize = textSize
            }
        Rectangle()
                .foregroundColor(.yellow)
            .frame(width: commonSize.width, height: commonSize.height)
        }
    }
}
Paul B
  • 3,989
  • 33
  • 46
0

There's a much simpler way to get the width of a view using GeometryReader. You need to create a state variable to store the width, then surround the desired view with a GeometryReader, and set the width value to the geometry inside that width. For instace:

struct ContentView: View {
    @State var width: CGFloat = 0.00 // this variable stores the width we want to get
    
    var body: some View {
        VStack(alignment: .leading) {
            GeometryReader { geometry in
                Text(timer.name)
                    .font(.largeTitle)
                    .fontWeight(.heavy)
                    .onAppear {
                        self.width = geometry.size.width
                        print("text width: \(width)") // test
                    }
            } // in this case, we are reading the width of text
            Text(timer.time)
                .font(.largeTitle)
                .fontWeight(.heavy)
                .opacity(0.5)
        }
    }
}

Note that the width will change if the target's view also changes. If you want to store it, I would suggest using a let constant somewhere else. Hope that helps!

  • 1
    **// in this case, we are reading the width of text** No, we are not. We are reading size of VStack – Farid Feb 24 '23 at 16:37
0

In reference to Giuseppe Sapienza's answer, by adding an .onChange modifier to SizeCalculator

.onChange(of: proxy.size, { oldValue, newValue in
    size = proxy.size
})

the ViewModifier will respond dynamically to changes in the size of the view being measured.