How to detect when the size or position of a view changes
How to detect when the size or position of a view changes 관련
Updated for Xcode 16
New in iOS 18
SwiftUI’s onGeometryChange()
modifier lets us track when a view's frame, size, or safe area insets change, then take any action as a result. Additionally, this modifier will also report the initial value of whatever you're watching, so it's helpful for both one-off and continuous monitoring.
For example, we could print the frame of a view when it's first created and whenever it changes, like this:
Text("Hello, world")
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
print("Frame is now \(newValue)")
}
Frame refers to the view's size and position, which means that rotating your device or changing window size is likely to trigger the action closure.
Breaking this modifier down, you'll see it accepts three values:
- The type of value you're interested in. We're watching a
CGRect
in the code above, but you might also useDouble
if you only wanted one value, or perhaps useBool
if the thing you're watching can be boiled down to a simple true or false. - A transformation closure, which is given a
GeometryProxy
and should return a value of whatever you specified, e.g. aCGRect
. This is the value SwiftUI will watch for changes going forward. - An action closure, which will be triggered whenever the watched value changes.
Important
Although you can use the action closure to set view state, make sure you don't accidentally get stuck in a layout loop. For example, the code below gets into a loop because it tries to display the view's size in the view itself:
struct ContentView: View {
@State private var textFrame = CGRect.zero
var body: some View {
Text("Size is: \(textFrame)")
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
textFrame = newValue
}
}
}
Being able to track a view's size and position unlocks a variety of abilities, such as being able to create views elsewhere with a matched size. For example, in the code below the rectangle always has the same frame as the text view:
struct ContentView: View {
@State private var textFrame = CGRect.zero
@State private var textSize = 17.0
var body: some View {
VStack {
Text("Hello, world")
.font(.system(size: textSize))
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
textFrame = newValue
}
Rectangle()
.frame(width: textFrame.width, height: textFrame.height)
Slider(value: $textSize, in: 10...30)
.padding()
}
}
}