|
| 1 | +// |
| 2 | +// GeometryReader.swift |
| 3 | +// AndroidSwiftUICore |
| 4 | +// |
| 5 | +// Size feedback from layout back into Swift — the one place the data flows |
| 6 | +// backwards. Swift resolves a tree *before* Compose lays it out, so the size |
| 7 | +// can't be known on the first pass: the interpreter measures, reports the size |
| 8 | +// through a callback, and the resulting store update re-evaluates with the real |
| 9 | +// numbers. Rendering a GeometryReader therefore settles over two passes. |
| 10 | +// |
| 11 | +// The reported size comes from the *constraints the parent offers*, never from |
| 12 | +// the content, so measuring can't feed back into itself. The store also drops |
| 13 | +// a report that matches what it already holds, so a steady layout stops. |
| 14 | +// |
| 15 | + |
| 16 | +import Foundation |
| 17 | + |
| 18 | +/// The geometry of the space a `GeometryReader` was offered. |
| 19 | +public struct GeometryProxy: Sendable { |
| 20 | + public let size: CGSize |
| 21 | +} |
| 22 | + |
| 23 | +/// Holds the last size the interpreter reported for one GeometryReader, |
| 24 | +/// persisted at its identity path so it survives re-evaluation. |
| 25 | +internal final class GeometrySizeStore { |
| 26 | + |
| 27 | + private(set) var size = CGSize(width: 0, height: 0) |
| 28 | + var onChange: (() -> Void)? |
| 29 | + |
| 30 | + /// Records a `"<width>,<height>"` report, re-evaluating only on a change. |
| 31 | + func update(from payload: String) { |
| 32 | + let parts = payload.split(separator: ",").compactMap { Double($0) } |
| 33 | + guard parts.count == 2 else { return } |
| 34 | + guard parts[0] != size.width || parts[1] != size.height else { return } |
| 35 | + size = CGSize(width: parts[0], height: parts[1]) |
| 36 | + onChange?() |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +public struct GeometryReader<Content: View>: View { |
| 41 | + |
| 42 | + internal let content: (GeometryProxy) -> Content |
| 43 | + |
| 44 | + public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) { |
| 45 | + self.content = content |
| 46 | + } |
| 47 | + |
| 48 | + public typealias Body = Never |
| 49 | +} |
| 50 | + |
| 51 | +extension GeometryReader: PrimitiveView { |
| 52 | + |
| 53 | + public func _render(in context: ResolveContext) -> RenderNode { |
| 54 | + let store = context.storage.persistentObject(at: context.path + ".geometry") { |
| 55 | + GeometrySizeStore() |
| 56 | + } |
| 57 | + store.onChange = context.storage.onChange |
| 58 | + let id = context.callbacks.register(.string { [store] payload in |
| 59 | + store.update(from: payload) |
| 60 | + }) |
| 61 | + // first pass resolves against .zero; the report brings the real size |
| 62 | + let proxy = GeometryProxy(size: store.size) |
| 63 | + return RenderNode( |
| 64 | + type: "GeometryReader", |
| 65 | + id: context.path, |
| 66 | + props: ["onSize": .int(Int(id))], |
| 67 | + children: Evaluator.resolveChildren(content(proxy), context.descending("content")) |
| 68 | + ) |
| 69 | + } |
| 70 | +} |
0 commit comments