arsenius
arsenius

Reputation: 13256

How to get size of child?

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

Upvotes: 28

Views: 23030

Answers (7)

Benzy Neez
Benzy Neez

Reputation: 20892

iOS 16+

Here is a version of a view size reader that uses .onGeometryChange (backwards compatible with iOS 16.0). Although not apparent from its name, this modifier also reports the initial size. In other words, it does not only get triggered when the size changes.

  • In this version, the size reader is implemented as a ViewModifier.

  • The size is written directly to a binding, so no PreferenceKey is needed.

struct SizeReader: ViewModifier {
    @Binding var size: CGSize

    func body(content: Content) -> some View {
        content
            .onGeometryChange(for: CGSize.self) { proxy in
                proxy.size
            } action: { newVal in
                size = newVal
            }
    }
}

A view extension can also be added, as a convenience.

extension View {
    func sizeReader(size: Binding<CGSize>) -> some View {
        modifier(SizeReader(size: size))
    }
}

Example use:

struct ContentView: View {
    @State private var size = CGSize.zero

    var body: some View {
        VStack(spacing: 20) {
            Text("The quick brown fox\njumps over the lazy dog")
                .font(.title2)
                .multilineTextAlignment(.center)
                .padding()
                .border(.red)
                .sizeReader(size: $size)
            Text("Size: \(size.width) x \(size.height)")
        }
    }
}

Screenshot


Pre iOS 16

For earlier iOS versions, the view modifier SizeReader can be implemented using a GeometryReader instead:

content
    .background {
        GeometryReader { proxy in
            Color.clear
                .onAppear {
                    size = proxy.size
                }
                .onChange(of: proxy.size) { newVal in
                    size = newVal
                }
        }
    }

Upvotes: 4

SirDeleck
SirDeleck

Reputation: 331

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.

Upvotes: 2

Aqua
Aqua

Reputation: 754

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

Upvotes: 1

shingo.nakanishi
shingo.nakanishi

Reputation: 2807

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

Upvotes: 1

arsenius
arsenius

Reputation: 13256

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

Upvotes: 32

tadija
tadija

Reputation: 3261

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
}

Upvotes: 2

Wil Gieseler
Wil Gieseler

Reputation: 2173

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

Upvotes: 46

Related Questions