Skip to content

Commit 4dd605e

Browse files
authored
Add GeometryReader support (#475)
1 parent ca15b69 commit 4dd605e

File tree

7 files changed

+403
-17
lines changed

7 files changed

+403
-17
lines changed

Example/HostingExample/ViewController.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ class ViewController: NSViewController {
6666

6767
struct ContentView: View {
6868
var body: some View {
69-
FlowLayoutDemo()
70-
.frame(width: 500)
71-
.padding()
69+
GeometryReaderExample()
7270
}
7371
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// GeometryReaderUITests.swift
3+
// OpenSwiftUIUITests
4+
5+
import SnapshotTesting
6+
import Testing
7+
8+
@MainActor
9+
@Suite(.snapshots(record: .never, diffTool: diffTool))
10+
struct GeometryReaderUITests {
11+
@Test
12+
func centerView() {
13+
struct ContentView: View {
14+
var body: some View {
15+
GeometryReader { proxy in
16+
Color.blue
17+
.frame(
18+
width: proxy.size.width / 2,
19+
height: proxy.size.height / 2,
20+
)
21+
.frame(maxWidth: .infinity, maxHeight: .infinity)
22+
.background(Color.yellow.opacity(0.3))
23+
}
24+
}
25+
}
26+
openSwiftUIAssertSnapshot(of: ContentView())
27+
}
28+
29+
@Test
30+
func overlapView() {
31+
struct ContentView: View {
32+
var body: some View {
33+
GeometryReader { geometry in
34+
Color.blue
35+
.frame(
36+
width: geometry.size.width / 2,
37+
height: geometry.size.height / 2
38+
)
39+
Color.red
40+
.frame(
41+
width: geometry.size.width / 3,
42+
height: geometry.size.height / 3
43+
)
44+
}
45+
}
46+
}
47+
openSwiftUIAssertSnapshot(of: ContentView())
48+
}
49+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// GeometryReaderExample.swift
3+
// SharedExample
4+
5+
#if OPENSWIFTUI
6+
import OpenSwiftUI
7+
#else
8+
import SwiftUI
9+
#endif
10+
11+
// FIXME: SwiftUI yellow background will ignoreSafeArea while OpenSwiftUI will have it.
12+
// See #474
13+
struct GeometryReaderExample: View {
14+
var body: some View {
15+
GeometryReader { geometry in
16+
VStack {
17+
Color.blue
18+
.frame(
19+
width: geometry.size.width / 2,
20+
height: geometry.size.height / 2
21+
)
22+
}
23+
.frame(maxWidth: .infinity, maxHeight: .infinity)
24+
.background(Color.yellow.opacity(0.3))
25+
}
26+
}
27+
}
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
//
2+
// GeometryReader.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: WIP
7+
// ID: 7D6D22DF7076CCC1FC5284D8E2D1B049 (SwiftUICore)
8+
9+
public import Foundation
10+
package import OpenGraphShims
11+
import OpenSwiftUI_SPI
12+
13+
// MARK: - GeometryReader [WIP]
14+
15+
/// A container view that defines its content as a function of its own size and
16+
/// coordinate space.
17+
///
18+
/// This view returns a flexible preferred size to its parent layout.
19+
@available(OpenSwiftUI_v1_0, *)
20+
@frozen
21+
public struct GeometryReader<Content>: View, UnaryView, PrimitiveView where Content: View {
22+
public var content: (GeometryProxy) -> Content
23+
24+
@inlinable
25+
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
26+
self.content = content
27+
}
28+
29+
public nonisolated static func _makeView(
30+
view: _GraphValue<Self>,
31+
inputs: _ViewInputs,
32+
) -> _ViewOutputs {
33+
var inputs = inputs
34+
let child = Attribute(Child(
35+
view: view.value,
36+
size: inputs.size,
37+
position: inputs.position,
38+
transform: inputs.transform,
39+
environment: inputs.environment,
40+
safeAreaInsets: inputs.safeAreaInsets,
41+
seed: .zero
42+
))
43+
var geometry: Attribute<ViewGeometry>!
44+
if inputs.needsGeometry {
45+
let rootGeometry = Attribute(RootGeometry(
46+
layoutDirection: .init(inputs.layoutDirection),
47+
proposedSize: inputs.size
48+
))
49+
inputs.position = Attribute(LayoutPositionQuery(
50+
parentPosition: inputs.position,
51+
localPosition: rootGeometry.origin()
52+
))
53+
inputs.size = rootGeometry.size()
54+
geometry = rootGeometry
55+
}
56+
var outputs = _VariadicView.Tree._makeView(
57+
view: .init(child),
58+
inputs: inputs
59+
)
60+
if inputs.needsGeometry {
61+
geometry.mutateBody(as: RootGeometry.self, invalidating: true) { geometry in
62+
geometry.$childLayoutComputer = outputs.layoutComputer
63+
}
64+
}
65+
outputs.layoutComputer = nil
66+
return outputs
67+
}
68+
69+
private struct Child: StatefulRule, AsyncAttribute {
70+
@Attribute var view: GeometryReader
71+
@Attribute var size: ViewSize
72+
@Attribute var position: ViewOrigin
73+
@Attribute var transform: ViewTransform
74+
@Attribute var environment: EnvironmentValues
75+
@OptionalAttribute var safeAreaInsets: SafeAreaInsets?
76+
var seed: UInt32
77+
78+
typealias Value = _VariadicView.Tree<_LayoutRoot<GeometryReaderLayout>, Content>
79+
80+
mutating func updateValue() {
81+
seed &+= 1
82+
let proxy = GeometryProxy(
83+
owner: .current!,
84+
size: $size,
85+
environment: $environment,
86+
transform: $transform,
87+
position: $position,
88+
safeAreaInsets: $safeAreaInsets,
89+
seed: seed,
90+
)
91+
// TODO: Observation
92+
let content = view.content(proxy)
93+
value = .init(root: .init(GeometryReaderLayout()), content: content)
94+
}
95+
}
96+
}
97+
98+
@available(*, unavailable)
99+
extension GeometryReader: Sendable {}
100+
101+
// MARK: - GeometryProxy [WIP]
102+
103+
/// A proxy for access to the size and coordinate space (for anchor resolution)
104+
/// of the container view.
105+
@available(OpenSwiftUI_v1_0, *)
106+
public struct GeometryProxy {
107+
var owner: AnyWeakAttribute
108+
var _size: WeakAttribute<ViewSize>
109+
var _environment: WeakAttribute<EnvironmentValues>
110+
var _transform: WeakAttribute<ViewTransform>
111+
var _position: WeakAttribute<ViewOrigin>
112+
var _safeAreaInsets: WeakAttribute<SafeAreaInsets>
113+
var seed: UInt32
114+
115+
package init(
116+
owner: AnyAttribute,
117+
size: Attribute<ViewSize>,
118+
environment: Attribute<EnvironmentValues>,
119+
transform: Attribute<ViewTransform>,
120+
position: Attribute<ViewOrigin>,
121+
safeAreaInsets: Attribute<SafeAreaInsets>?,
122+
seed: UInt32,
123+
) {
124+
self.owner = AnyWeakAttribute(owner)
125+
self._size = WeakAttribute(size)
126+
self._environment = WeakAttribute(environment)
127+
self._transform = WeakAttribute(transform)
128+
self._position = WeakAttribute(position)
129+
self._safeAreaInsets = WeakAttribute(safeAreaInsets)
130+
self.seed = seed
131+
}
132+
133+
package var context: AnyRuleContext {
134+
AnyRuleContext(attribute: owner.attribute ?? .nil)
135+
}
136+
137+
/// The size of the container view.
138+
public var size: CGSize {
139+
Update.perform {
140+
guard let size = _size.attribute else {
141+
return .zero
142+
}
143+
return context[size].value
144+
}
145+
}
146+
147+
private var placementContext: _PositionAwarePlacementContext? {
148+
Update.assertIsLocked()
149+
guard let owner = owner.attribute,
150+
let size = _size.attribute,
151+
let environment = _environment.attribute,
152+
let transform = _transform.attribute,
153+
let position = _position.attribute
154+
else {
155+
return nil
156+
}
157+
return _PositionAwarePlacementContext(
158+
context: .init(attribute: owner),
159+
size: size,
160+
environment: environment,
161+
transform: transform,
162+
position: position,
163+
safeAreaInsets: .init(_safeAreaInsets.attribute),
164+
)
165+
}
166+
167+
/// Resolves the value of an anchor to the container view.
168+
public subscript<T>(anchor: Anchor<T>) -> T {
169+
_openSwiftUIUnimplementedFailure()
170+
}
171+
172+
/// The safe area inset of the container view.
173+
public var safeAreaInsets: EdgeInsets {
174+
Update.perform {
175+
guard let placementContext else {
176+
return .zero
177+
}
178+
return placementContext.safeAreaInsets()
179+
}
180+
}
181+
182+
/// Returns the container view's bounds rectangle, converted to a defined
183+
/// coordinate space.
184+
@available(OpenSwiftUI_v1_0, *)
185+
@available(*, deprecated, message: "use overload that accepts a CoordinateSpaceProtocol instead")
186+
@_disfavoredOverload
187+
public func frame(in coordinateSpace: CoordinateSpace) -> CGRect {
188+
let size = size
189+
return Update.perform {
190+
guard let placementContext else {
191+
return .zero
192+
}
193+
var rect = CGRect(origin: .zero, size: size)
194+
rect.convert(from: placementContext, to: coordinateSpace)
195+
return rect
196+
}
197+
}
198+
199+
@_spi(Private)
200+
public func frameClippedToScrollViews(in space: CoordinateSpace) -> (frame: CGRect, exact: Bool) {
201+
_openSwiftUIUnimplementedFailure()
202+
}
203+
204+
package func rect(_ r: CGRect, in coordinateSpace: CoordinateSpace) -> CGRect {
205+
_openSwiftUIUnimplementedFailure()
206+
}
207+
208+
package var transform: ViewTransform {
209+
_openSwiftUIUnimplementedFailure()
210+
}
211+
212+
package var environment: EnvironmentValues {
213+
Update.perform {
214+
guard let environment = _environment.attribute else {
215+
return EnvironmentValues()
216+
}
217+
return context[environment]
218+
}
219+
}
220+
221+
package static var current: GeometryProxy? {
222+
if let data = _threadGeometryProxyData() {
223+
data.assumingMemoryBound(to: GeometryProxy.self).pointee
224+
} else {
225+
nil
226+
}
227+
}
228+
229+
package func asCurrent<Result>(do body: () throws -> Result) rethrows -> Result {
230+
let old = _threadGeometryProxyData()
231+
defer { _setThreadGeometryProxyData(old) }
232+
return try withUnsafePointer(to: self) { ptr in
233+
_setThreadGeometryProxyData(.init(mutating: ptr))
234+
return try body()
235+
}
236+
}
237+
}
238+
239+
@available(OpenSwiftUI_v5_0, *)
240+
extension GeometryProxy {
241+
/// Returns the given coordinate space's bounds rectangle, converted to the
242+
/// local coordinate space.
243+
public func bounds(of coordinateSpace: NamedCoordinateSpace) -> CGRect? {
244+
_openSwiftUIUnimplementedFailure()
245+
}
246+
247+
/// Returns the container view's bounds rectangle, converted to a defined
248+
/// coordinate space.
249+
public func frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRect {
250+
_openSwiftUIUnimplementedFailure()
251+
}
252+
}
253+
254+
@available(OpenSwiftUI_v6_0, *)
255+
extension GeometryProxy {
256+
package func convert(
257+
globalPoint: CGPoint,
258+
to coordinateSpace: some CoordinateSpaceProtocol,
259+
) -> CGPoint {
260+
_openSwiftUIUnimplementedFailure()
261+
}
262+
}
263+
264+
@available(*, unavailable)
265+
extension GeometryProxy: Sendable {}
266+
267+
// MARK: - GeometryReaderLayout
268+
269+
private struct GeometryReaderLayout: Layout {
270+
static var layoutProperties: LayoutProperties {
271+
var properties = LayoutProperties()
272+
if !isLinkedOnOrAfter(.v2) {
273+
properties.isDefaultEmptyLayout = true
274+
properties.isIdentityUnaryLayout = true
275+
}
276+
return properties
277+
}
278+
279+
func sizeThatFits(
280+
proposal: ProposedViewSize,
281+
subviews: Subviews,
282+
cache: inout (),
283+
) -> CGSize {
284+
proposal.replacingUnspecifiedDimensions(by: .zero)
285+
}
286+
287+
func placeSubviews(
288+
in bounds: CGRect,
289+
proposal: ProposedViewSize,
290+
subviews: Subviews,
291+
cache: inout (),
292+
) {
293+
guard !subviews.isEmpty else {
294+
return
295+
}
296+
let anchor = UnitPoint.topLeading
297+
for subview in subviews {
298+
let dimensions = subview.dimensions(in: .init(bounds.size))
299+
subview.place(
300+
at: bounds.origin,
301+
anchor: anchor,
302+
dimensions: dimensions,
303+
)
304+
}
305+
}
306+
}

0 commit comments

Comments
 (0)