One way to accomplish this is utilising "anchor preferences".
The idea is, to create the bounds anchor of the button when it is created and store it into an anchor preference.
To get an actual bounds value, we need a GeometryProxy where we relate the bounds anchor and get the bounds value.
When we have the bounds value, we store it in a state variable where they are accessible when the button action executes.
The following solution creates a number of buttons where the bounds are accessible via a Dictionary where the key is the button's label.
import SwiftUI
struct ContentView: View {
let labels = (0...4).map { "- \($0) -" }
@State private var bounds: [String: CGRect] = [:]
var body: some View {
VStack {
ForEach(labels, id: \.self) { label in
Button(action: {
let bounds = bounds[label]
print(bounds ?? "")
}) {
Text(verbatim: label)
}
// Store bounds anchors into BoundsAnchorsPreferenceKey:
.anchorPreference(
key: BoundsAnchorsPreferenceKey.self,
value: .bounds,
transform: { [label: $0] })
}
}
.frame(width: 300, height: 300, alignment: .center)
.backgroundPreferenceValue(BoundsAnchorsPreferenceKey.self) { anchors in
// Get bounds relative to VStack:
GeometryReader { proxy in
let localBoundss = anchors.mapValues { anchor in
CGRect(origin: proxy[anchor].origin, size: proxy[anchor].size)
}
Color.clear.border(Color.blue, width: 1)
.preference(key: BoundsPreferenceKey.self, value: localBoundss)
}
}
.onPreferenceChange(BoundsPreferenceKey.self) { bounds in
// Store bounds into the state variable:
self.bounds = bounds
}
}
}
extension CGRect: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(origin.x)
hasher.combine(origin.y)
hasher.combine(size.width)
hasher.combine(size.height)
}
}
struct BoundsAnchorsPreferenceKey: PreferenceKey {
typealias Value = [String: Anchor<CGRect>]
static var defaultValue: Value = [:]
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}
struct BoundsPreferenceKey: PreferenceKey {
typealias Value = [String: CGRect]
static var defaultValue: Value = [:]
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}
.navigationViewStyle(.stack)
)
The solution as is, looks a bit elaborated - but doesn't use any "tricks". We may alleviate this somewhat with using ViewModifiers.