Reputation: 13256
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())
Upvotes: 28
Views: 23030
Reputation: 20892
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)")
}
}
}
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
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
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
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
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
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
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