hoshy
hoshy

Reputation: 563

Observe frame changes in SwiftUI

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

Answers (2)

alex oliveira
alex oliveira

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

Asperi
Asperi

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

Related Questions