33

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
  • 17,605
  • 10
  • 69
  • 79
Alexey Primechaev
  • 576
  • 1
  • 4
  • 12

2 Answers2

45

The only way to get the dimensions of a View is by using a GeometryReader. The reader returns the dimensions of the container.

What is a geometry reader? the documentation says:

A container view that defines its content as a function of its own size and coordinate space. Apple Doc

So you could get the dimensions by doing this:

struct ContentView: View {
    
   @State var frame: CGSize = .zero
    
    var body: some View {
        HStack {
            GeometryReader { (geometry) in
                self.makeView(geometry)
            }
        }
        
    }
    
    func makeView(_ geometry: GeometryProxy) -> some View {
        print(geometry.size.width, geometry.size.height)

        DispatchQueue.main.async { self.frame = geometry.size }

        return Text("Test")
                .frame(width: geometry.size.width)
    }
}

The printed size is the dimension of the HStack that is the container of inner view.

You could potentially using another GeometryReader to get the inner dimension.

But remember, SwiftUI is a declarative framework. So you should avoid calculating dimensions for the view:

read this to more example:

Giuseppe Sapienza
  • 3,445
  • 20
  • 21
  • 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
  • 2
    @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
25

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 to way 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
  • 2,408
  • 21
  • 33