27

Is there any way to get the size of a child view in SwiftUI?

I'm basically looking to do the UIKit equivalent of:

self.child.frame.origin.x -= self.child.intrinsicContentSize.width/2.0

I don't think a GeometryReader would work since that returns the available size in the parent.

[Edit] I've found it's possible to get and save the dimensions using .alignmentGuide(_, computeValue:) though that's definitely a hack.

LessonSliderText(text: self.textForProgress(self.progress), color: self.completedColor)
    .alignmentGuide(HorizontalAlignment.leading) { (dimensions) -> Length in
        self.textSize = CGSize(width: dimensions.width, height: dimensions.height)
        return 0
    }
    .offset(x: self.width*self.currentPercentage - self.textSize.width / 2.0)
    .offset(y: -self.textSize.height/2.0)
    .animation(nil)
    .opacity(self.isDragging ? 1.0 : 0.0)
    .animation(.basic())

What I'm trying to accomplish

Ky -
  • 30,724
  • 51
  • 192
  • 308
arsenius
  • 12,090
  • 7
  • 58
  • 76
  • Can you show your code and explain what you would like your view to be. – Fogmeister Jun 13 '19 at 06:24
  • 1
    Can you show us what you want to achieve in a drawing or something? I think you're maybe misunderstanding how SwiftUI does it's layout.... – Tycho Pandelaar Jun 13 '19 at 08:26
  • 1
    I attached an image already. I'm trying to position that text bubble thing above the line and centered behind the white circle. Without affecting the layout of the line. The code I pasted does accomplish that. – arsenius Jun 13 '19 at 09:04
  • Am I right? You need to have a bubble the bigger than your text? And with rounded corners? – DenFav Jun 13 '19 at 11:07
  • You can use AnchorPreferences to bubble information about a child view's geometry up to a parent view. See: https://swiftui-lab.com/communicating-with-the-view-tree-part-2/ – Tylerc230 Nov 13 '19 at 22:29
  • Does this answer your question? [Scaling down a text's font size to fit its length to another text in SwiftUI](https://stackoverflow.com/questions/69058317/scaling-down-a-texts-font-size-to-fit-its-length-to-another-text-in-swiftui) – lorem ipsum Sep 04 '21 at 21:19

6 Answers6

43

Updated and generalized @arsenius code. Now you can easily bind a parent view's state variable.

struct ChildSizeReader<Content: View>: View {
    @Binding var size: CGSize
    let content: () -> Content
    var body: some View {
        ZStack {
            content()
                .background(
                    GeometryReader { proxy in
                        Color.clear
                            .preference(key: SizePreferenceKey.self, value: proxy.size)
                    }
                )
        }
        .onPreferenceChange(SizePreferenceKey.self) { preferences in
            self.size = preferences
        }
    }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero

    static func reduce(value _: inout Value, nextValue: () -> Value) {
        _ = nextValue()
    }
}

Usage:

struct ChildSizeReaderExample: View {
    @State var textSize: CGSize = .zero
    var body: some View {
        VStack {
            ChildSizeReader(size: $textSize) {
                Text("Hello I am some arbitrary text.")
            }
            Text("My size is \(textSize.debugDescription)!")
        }
    }
}
Wil Gieseler
  • 1,893
  • 1
  • 17
  • 18
  • Great! Would anything change if the child is a `List`'s iterated row if you only need one and they're all the same size? applying the `ChildSizeReader` to all of the rows would probably be overkill. – Martin Nov 01 '21 at 00:08
  • 3
    FYI, ran into a infinite loop issue with this code when the proxy came back with a width/height like 41.3333333329, then the preference would have it as 41.3333333333. Fixed it by rounding the width & height to make sure it stays consistent. `content().frame(width: size != .zero ? floor(size.width) : nil, height: size != .zero ? floor(size.height) : nil)` – robhasacamera Jul 31 '22 at 04:32
  • 2
    @robhasacamera this helped me out a ton! how did you even discover that this was causing an infinite loop? – meowmeowmeow Feb 17 '23 at 04:09
  • 1
    @meowmeowmeow I ran into it while using this solution. Can’t remember if I used breakpoints or a debug label but I eventually noticed the size was slightly off. – robhasacamera Feb 18 '23 at 15:41
29

Basically, the answer at this point is to use a GeometryReader inside of the child's background(...) modifier.

// This won't be valid until the first layout pass is complete
@State var childSize: CGSize = .zero

var body: some View {
    ZStack {
        Text("Hello World!")
            .background(
                GeometryReader { proxy in
                    Color.clear
                       .preference(
                           key: SizePreferenceKey.self, 
                           value: proxy.size
                        )
                }
            )
      }
      .onPreferenceChange(SizePreferenceKey.self) { preferences in
          self.childSize = preferences
      }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero

    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue()
    }
}     
Community
  • 1
  • 1
arsenius
  • 12,090
  • 7
  • 58
  • 76
2

Here's a reusable variant of the accepted answer:

public protocol CGSizePreferenceKey: PreferenceKey where Value == CGSize {}

public extension CGSizePreferenceKey {
    static func reduce(value _: inout CGSize, nextValue: () -> CGSize) {
        _ = nextValue()
    }
}

public extension View {
    func onSizeChanged<Key: CGSizePreferenceKey>(
        _ key: Key.Type,
        perform action: @escaping (CGSize) -> Void) -> some View
    {
        self.background(GeometryReader { geo in
            Color.clear
                .preference(key: Key.self, value: geo.size)
        })
        .onPreferenceChange(key) { value in
            action(value)
        }
    }
}

Usage:

struct Example: View {
    var body: some View {
        Text("Hello, World!")
            .onSizeChanged(ExampleViewSize.self) { size in
                print("size: \(size)")
            }
    }
}

struct ExampleViewSize: CGSizePreferenceKey {
    static var defaultValue: CGSize = .zero
}
tadija
  • 2,981
  • 26
  • 37
2

Adding GeometryReader to the background of a view and measuring size of Color.clear works but seems hacky to me. I have found different approach that I would like to share.

struct SomeView: View {

    @State
    var bounds: CGRect = .zero

    var body: some View {
        GeometryReader { geometry in
            Text("Hello Stack Overflow")
                .anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { geometry[$0] }
                .onPreferenceChange(BoundsPreferenceKey.self) { bounds = $0 }
        }
    }

}

private struct BoundsPreferenceKey: PreferenceKey {

    static var defaultValue: CGRect = .zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }

}

In this approach you surround child view with GeometryReader and use anchor preference to get CGRect that contains position and size of the view in GeometryReader coordinates.

One potential disadvantage of the solution is that GeometryReader can mess up your layout if you haven't plan for it, as it will expand to take all available space. However if you need child view size for layout purposes there is a good chance you already use GeometryReader to measure parent view size.

SirDeleck
  • 121
  • 1
  • 6
  • I think this is the right answer. To address your concern re: the greediness of the GeometryReader, you can sometimes modify it so that it will exactly hug the contents: GeometryReader{...}.frame(width: bounds.width, height: bounds.height) – tway Dec 31 '21 at 05:24
1

I referred to all of the following codes that have already been answered.

Custom modifier of this answer:

extension View {
  func size(size: Binding<CGSize>) -> some View {
    ChildSizeReader(size: size) {
      self
    }
  }
}

As this comment says, I don't think ZStack is necessary, so I also post a version with ZStack removed.

All Code:

import SwiftUI

struct ContentView: View {
  var body: some View {
    ChildSizeReaderExample()
  }
}

struct ChildSizeReaderExample: View {
  @State var textSize: CGSize = .zero

  var body: some View {
    VStack {
      Text("Hello I am some arbitrary text.").size(size: $textSize) // Usage
      Text("My size is \(textSize.debugDescription)")
    }
  }
}

struct ChildSizeReader<Content: View>: View {
  @Binding var size: CGSize

  let content: () -> Content
  var body: some View {
    // Remove ZStack from the existing answer.
    content().background(
      GeometryReader { proxy in
        Color.clear.preference(
          key: SizePreferenceKey.self,
          value: proxy.size
        )
      }
    )
    .onPreferenceChange(SizePreferenceKey.self) { preferences in
      self.size = preferences
    }
  }
}

struct SizePreferenceKey: PreferenceKey {
  typealias Value = CGSize
  static var defaultValue: Value = .zero

  static func reduce(value _: inout Value, nextValue: () -> Value) {
    _ = nextValue()
  }
}

extension View {
  func size(size: Binding<CGSize>) -> some View {
    ChildSizeReader(size: size) {
      self
    }
  }
}
shingo.nakanishi
  • 2,045
  • 2
  • 21
  • 55
1
// SizeModifier.swift
import Foundation
import SwiftUI

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

struct SizeModifer: ViewModifier {
    
    private var sizeView: some View {
        GeometryReader { geometry in
            Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
        }
    }
    
    func body(content: Content) -> some View {
        content
            .background(sizeView)
    }
    
}

extension View {
    func onSizeChanged(_ handler: @escaping (CGSize) -> Void) -> some View {
        self
            .modifier(SizeModifer())
            .onPreferenceChange(SizePreferenceKey.self, perform: { value in
                handler(value)
            })
    }
}

Here is how to use it:

// ContentView
import SwiftUI

struct ContentView: View {
    
    @State private var childSize: CGSize = .zero
    
    var body: some View {
        Text("My size \(childSize.width)x\(childSize.height)")
            .padding()
            .onSizeChanged { size in
                childSize = size
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Aqua
  • 716
  • 8
  • 10