Get CGRect of View
Asked Answered
S

2

3

I'm using a "RectGetter" to get the CGRect of a View.

Like this:

Text("Hello")
    .background(RectGetter(rect: self.$rect))


struct RectGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { proxy in
            self.createView(proxy: proxy)
        }
    }

    func createView(proxy: GeometryProxy) -> some View {
        DispatchQueue.main.async {
            self.rect = proxy.frame(in: .global) // crash here
        }

        return Rectangle().fill(Color.clear)
    }
}

But sometimes I get a crash when setting the rect.

Thread 0 name:
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x00000001a1ec5ec4 __pthread_kill + 8
1   libsystem_pthread.dylib         0x00000001a1de5724 pthread_kill$VARIANT$armv81 + 216 (pthread.c:1458)
2   libsystem_c.dylib               0x00000001a1d358c0 __abort + 112 (abort.c:147)
3   libsystem_c.dylib               0x00000001a1d35850 abort + 112 (abort.c:118)
4   AttributeGraph                  0x00000001cce56548 0x1cce25000 + 202056
5   AttributeGraph                  0x00000001cce2b980 0x1cce25000 + 27008
6   SwiftUI                         0x00000001d89ce9b4 $s7SwiftUI27_PositionAwareLayoutContextV10dimensionsSo6CGSizeVvg + 56 (<compiler-generated>:0)
7   SwiftUI                         0x00000001d8a43584 $sSo6CGSizeVIgd_ABIegr_TRTA + 24 (<compiler-generated>:0)
8   SwiftUI                         0x00000001d8a43a54 $s7SwiftUI13GeometryProxyV4sync33_4C4BAC551E328ACCA9CD3748EDC0CC3ALLyxSgxyXElFxAA9ViewGraphCXEfU_... + 92 (GeometryReader.swift:126)
9   SwiftUI                         0x00000001d8a43f20 $s7SwiftUI13GeometryProxyV4sync33_4C4BAC551E328ACCA9CD3748EDC0CC3ALLyxSgxyXElFxAA9ViewGraphCXEfU_... + 20 (<compiler-generated>:0)
10  SwiftUI                         0x00000001d8c4842c $s7SwiftUI16ViewRendererHostPAAE06updateC5Graph4bodyqd__qd__AA0cG0CXE_tlF + 80 (ViewRendererHost.swift:64)
11  SwiftUI                         0x00000001d8a438d4 $s7SwiftUI13GeometryProxyV5frame2inSo6CGRectVAA15CoordinateSpaceO_tF + 196 (GeometryReader.swift:125)
12  MyApp                           0x0000000102cf5c54 closure #1 in RectGetter.createView(proxy:) + 128 (RectGetter.swift:22)

Is there some more reliable way (not crashing) to get the CGRect of a View?

Sweeper answered 13/2, 2020 at 18:49 Comment(6)
The problem isn't getting the CGRect; it's what you do with it. You're reaching out and setting it elsewhere. This generally isn't how you want to do layout in SwiftUI (the typical tool here is a Preference, Anchor, or Alignment, not .async setters). These views are not classes; they're structs. There's no promise that self will exist in a meaningful way by the time main.async is called. It's unclear what self.$rect is in this case and what it causes to happen next. What layout are you trying to achieve?Aesculapius
self.$rect is a @State var rect = CGRect.zero... I need the CGrect for positioning. I don't think above usage is out of the ordinary, I snagged it from some SwiftUI blogs.Sweeper
I've seen it in a number of blogs, but I haven't seen it from Apple, and I don't think it's based on anything that's promised to work. It just happens to work (Asperi's answer is good in that it evaluates the geometry at a time when it exists, but it still is relying on behavior that I've never seen Apple suggest is promised.) SwiftUI-Lab has a very good series on how to do declarative layout in SwiftUI. swiftui-lab.com/alignment-guides swiftui-lab.com/communicating-with-the-view-tree-part-1Aesculapius
Also specifically to this point: swiftui-lab.com/state-changes I've typically found that if the goal is layout, then alignment guides are the tool you really want rather than "lay it out, let a state variable figure things out, and then lay it out again" which I suspect is what you're doing here (I've done it myself before I rewrote everything to use AlignmentGuides).Aesculapius
(To be clear, I'm not certain that these async setters are invalid. Clearly we can do async work and have that work set State variables, and that's fully supported. But layout causing a setter to re-layout seems to violate the declarative nature of SwiftUI, and it's not clear whether it's a hack that may break, a bad practice that happens to rely on solid footing, or actually fine. SwiftUI-Lab has been the most reliable explorer I've found into these questions, though.Aesculapius
Thanks Rob for your elaborate comments, it's true there's no official word from Apple on this (as much of the documentation on SwiftUI). AlignmentGuides might be the correct approach but right now I'm a bit lazy to learn that API so I'll just try Asperis suggestion.Sweeper
M
13

Update: improved and simplified a possible solution to read rect of any view in any coordinate space via helper extension. Works since Xcode 11.1, retested with Xcode 13.3.

Main part:

func rectReader(_ binding: Binding<CGRect>, _ space: CoordinateSpace = .global) -> some View {
    GeometryReader { (geometry) -> Color in
        let rect = geometry.frame(in: space)
        DispatchQueue.main.async {
            binding.wrappedValue = rect
        }
        return .clear
    }
}

Usage the same

Text("Test").background(rectReader($rect))

or with new extension

Text("Test").reading(rect: $rect)

Complete findings and code is here

Measly answered 13/2, 2020 at 18:57 Comment(0)
N
0

Another way is with Preference Keys, as outlined here.

In summary, you can set up a modifier:

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGRect = .zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

struct SizeModifier: ViewModifier {
    private var sizeView: some View {
        GeometryReader { geometry in
            Color.clear.preference(key: SizePreferenceKey.self, value: geometry.frame(in: .global))
        }
    }

    func body(content: Content) -> some View {
        content.background(sizeView)
    }
}

Then use that one:

SomeView()
   .modifier(SizeModifier())
   .onPreferenceChange(SizePreferenceKey.self) { print("My rect is: \($0)") }
Nevil answered 5/5, 2022 at 4:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.