Skip to content

Commit f58b757

Browse files
committed
Implement GeometryReader (addresses #181)
1 parent 361e64a commit f58b757

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// A proxy for querying a view's geometry. See ``GeometryReader``.
2+
public struct GeometryProxy {
3+
/// The size proposed to the view by its parent. In the context of
4+
/// ``GeometryReader``, this is the size that the ``GeometryReader``
5+
/// will take on (to prevent feedback loops).
6+
public var size: SIMD2<Int>
7+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/// A container view that allows its content to read the size proposed to it.
2+
///
3+
/// Geometry readers always take up the size proposed to them; no more, no less.
4+
/// This is to decouple the geometry reader's size from the size of its content
5+
/// in order to avoid feedback loops.
6+
///
7+
/// ```swift
8+
/// struct MeasurementView: View {
9+
/// var body: some View {
10+
/// GeometryReader { proxy in
11+
/// Text("Width: \(proxy.size.x)")
12+
/// Text("Height: \(proxy.size.y)")
13+
/// }
14+
/// }
15+
/// }
16+
/// ```
17+
///
18+
/// > Note: Geometry reader content may get evaluated multiple times with various
19+
/// > sizes before the layout system settles on a size. Do not depend on the size
20+
/// > proposal always being final.
21+
public struct GeometryReader<Content: View>: TypeSafeView, View {
22+
var content: (GeometryProxy) -> Content
23+
24+
public var body = EmptyView()
25+
26+
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
27+
self.content = content
28+
}
29+
30+
func children<Backend: AppBackend>(
31+
backend: Backend,
32+
snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
33+
environment: EnvironmentValues
34+
) -> GeometryReaderChildren<Content> {
35+
GeometryReaderChildren()
36+
}
37+
38+
func layoutableChildren<Backend: AppBackend>(
39+
backend: Backend,
40+
children: GeometryReaderChildren<Content>
41+
) -> [LayoutSystem.LayoutableChild] {
42+
[]
43+
}
44+
45+
func asWidget<Backend: AppBackend>(
46+
_ children: GeometryReaderChildren<Content>,
47+
backend: Backend
48+
) -> Backend.Widget {
49+
// This is a little different to our usual wrapper implementations
50+
// because we want to avoid calling the user's content closure before
51+
// we actually have to.
52+
return backend.createContainer()
53+
}
54+
55+
func update<Backend: AppBackend>(
56+
_ widget: Backend.Widget,
57+
children: GeometryReaderChildren<Content>,
58+
proposedSize: SIMD2<Int>,
59+
environment: EnvironmentValues,
60+
backend: Backend,
61+
dryRun: Bool
62+
) -> ViewUpdateResult {
63+
let view = content(GeometryProxy(size: proposedSize))
64+
65+
let environment = environment.with(\.layoutAlignment, .leading)
66+
67+
let contentNode: AnyViewGraphNode<Content>
68+
if let node = children.node {
69+
contentNode = node
70+
} else {
71+
contentNode = AnyViewGraphNode(
72+
for: view,
73+
backend: backend,
74+
environment: environment
75+
)
76+
children.node = contentNode
77+
78+
// It's ok to add the child here even though it's not a dry run
79+
// because this is guaranteed to only happen once. Dry runs are
80+
// more about 'commit' actions that happen every single update.
81+
backend.addChild(contentNode.widget.into(), to: widget)
82+
}
83+
84+
// TODO: Look into moving this to the final non-dry run update. In order
85+
// to do so we'd have to give up on preferences being allowed to affect
86+
// layout (which is probably something we don't want to support anyway
87+
// because it sounds like feedback loop central).
88+
let contentResult = contentNode.update(
89+
with: view,
90+
proposedSize: proposedSize,
91+
environment: environment,
92+
dryRun: dryRun
93+
)
94+
95+
if !dryRun {
96+
backend.setPosition(ofChildAt: 0, in: widget, to: .zero)
97+
backend.setSize(of: widget, to: proposedSize)
98+
}
99+
100+
return ViewUpdateResult(
101+
size: ViewSize(
102+
size: proposedSize,
103+
idealSize: SIMD2(10, 10),
104+
minimumWidth: 0,
105+
minimumHeight: 0,
106+
maximumWidth: nil,
107+
maximumHeight: nil
108+
),
109+
childResults: [contentResult]
110+
)
111+
}
112+
}
113+
114+
class GeometryReaderChildren<Content: View>: ViewGraphNodeChildren {
115+
var node: AnyViewGraphNode<Content>?
116+
117+
var widgets: [AnyWidget] {
118+
[node?.widget].compactMap { $0 }
119+
}
120+
121+
var erasedNodes: [ErasedViewGraphNode] {
122+
[node.map(ErasedViewGraphNode.init(wrapping:))].compactMap { $0 }
123+
}
124+
}

0 commit comments

Comments
 (0)