Reputation: 563
I have view that can be dragged and dropped on top of other views (lets say categories). To detect which category view I'm on top of, I store their frames in a frames array, which happens in onAppear of their invisible overlays. (This is based on Paul Hudsons implementation in this tutorial).
This works all nice and well, except when the position of those views change, e.g. in device orientation or windows resizing on iPad. This of course doesn't trigger onAppear, so the frames don't match anymore.
HStack() {
ForEach(categories) { category in
ZStack {
Circle()
Rectangle()
.foregroundColor(.clear)
.overlay(
GeometryReader { geo in
Color.clear
.onAppear {
categoryFrames[index(for: category)] = geo.frame(in: .global)
}
}
)
}
}
}
So any idea how to update the frames in those instances or how to differently observe them would be welcome.
Upvotes: 8
Views: 8362
Reputation: 561
I had a similar problem and this post inspired me in finding a solution. So maybe this will be useful to someone else.
Just assign to the onChange
modifier the same you did to onAppear
and set it to fire when geo.size
changes.
HStack() {
ForEach(categories) { category in
ZStack {
Circle()
Rectangle()
.foregroundColor(.clear)
.overlay(
GeometryReader { geo in
Color.clear
.onAppear {
categoryFrames[index(for: category)] = geo.frame(in: .global)
}
.onChange(of: geo.size) { _ in
categoryFrames[index(for: category)] = geo.frame(in: .global)
}
}
)
}
}
}
Upvotes: 15
Reputation: 257819
It is possible to read views frames dynamically during refresh using view preferences, so you don't care about orientation, because have actual frames every time view is redrawn.
Here is a draft of approach.
Introduce model for view preference key:
struct ItemRec: Equatable {
let i: Int // item index
let p: CGRect // item position frame
}
struct ItemPositionsKey: PreferenceKey {
typealias Value = [ItemRec]
static var defaultValue = Value()
static func reduce(value: inout Value, nextValue: () -> Value) {
value.append(contentsOf: nextValue())
}
}
and now your code (assuming @State private var categoryFrames = [Int, CGRect]()
)
HStack() {
ForEach(categories) { category in
ZStack {
Circle()
Rectangle()
.foregroundColor(.clear)
.background( // << prefer background to avoid any side effect
GeometryReader { geo in
Color.clear.preference(key: ItemPositionsKey.self,
value: [ItemRec(i: index(for: category), p: geo.frame(in: .global))])
}
)
}
}
.onPreferenceChange(ItemPositionsKey.self) {
// actually you can use this listener at any this view hierarchy level
// and possibly use directly w/o categoryFrames state
for item in $0 {
categoryFrames[item.i] = item.p
}
}
}
Upvotes: 4