Skip to content

Commit e7a66ff

Browse files
committed
Split View.update into View.computeLayout and View.commit
1 parent 84015e4 commit e7a66ff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2094
-1367
lines changed

Examples/Sources/WindowingExample/WindowingApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ struct WindowingApp: App {
8484
}
8585

8686
Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
87+
.resizable()
88+
.aspectRatio(contentMode: .fit)
8789

8890
Divider()
8991

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ public final class AppKitBackend: AppBackend {
2929
// We assume that all scrollers have their controlSize set to `.regular` by default.
3030
// The internet seems to indicate that this is true regardless of any system wide
3131
// preferences etc.
32-
Int(
32+
let result = Int(
3333
NSScroller.scrollerWidth(
3434
for: .regular,
3535
scrollerStyle: NSScroller.preferredScrollerStyle
3636
).rounded(.awayFromZero)
3737
)
38+
print(result)
39+
return result
3840
}
3941

4042
private let appDelegate = NSCustomApplicationDelegate()

Sources/AppKitBackend/NSViewRepresentable.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,14 @@ extension View where Self: NSViewRepresentable {
139139
}
140140
}
141141

142-
public func update<Backend: AppBackend>(
142+
public func computeLayout<Backend: AppBackend>(
143143
_ widget: Backend.Widget,
144144
children: any ViewGraphNodeChildren,
145145
proposedSize: SIMD2<Int>,
146146
environment: EnvironmentValues,
147-
backend: Backend,
148-
dryRun: Bool
149-
) -> ViewUpdateResult {
150-
guard let backend = backend as? AppKitBackend else {
147+
backend: Backend
148+
) -> ViewLayoutResult {
149+
guard backend is AppKitBackend else {
151150
fatalError("NSViewRepresentable updated by \(Backend.self)")
152151
}
153152

@@ -160,11 +159,17 @@ extension View where Self: NSViewRepresentable {
160159
context: representingWidget.context!
161160
)
162161

163-
if !dryRun {
164-
backend.setSize(of: representingWidget, to: size.size)
165-
}
162+
return ViewLayoutResult.leafView(size: size)
163+
}
166164

167-
return ViewUpdateResult.leafView(size: size)
165+
public func commit<Backend: AppBackend>(
166+
_ widget: Backend.Widget,
167+
children: any ViewGraphNodeChildren,
168+
layout: ViewLayoutResult,
169+
environment: EnvironmentValues,
170+
backend: Backend
171+
) {
172+
backend.setSize(of: widget, to: layout.size.size)
168173
}
169174
}
170175

Sources/SwiftCrossUI/Layout/LayoutSystem.swift

Lines changed: 89 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,34 @@ public enum LayoutSystem {
4545
}
4646

4747
public struct LayoutableChild {
48-
private var update:
48+
private var computeLayout:
4949
(
5050
_ proposedSize: SIMD2<Int>,
51-
_ environment: EnvironmentValues,
52-
_ dryRun: Bool
53-
) -> ViewUpdateResult
51+
_ environment: EnvironmentValues
52+
) -> ViewLayoutResult
53+
private var _commit: () -> ViewLayoutResult
5454
var tag: String?
5555

5656
public init(
57-
update: @escaping (SIMD2<Int>, EnvironmentValues, Bool) -> ViewUpdateResult,
57+
computeLayout: @escaping (SIMD2<Int>, EnvironmentValues) -> ViewLayoutResult,
58+
commit: @escaping () -> ViewLayoutResult,
5859
tag: String? = nil
5960
) {
60-
self.update = update
61+
self.computeLayout = computeLayout
62+
self._commit = commit
6163
self.tag = tag
6264
}
6365

64-
public func update(
66+
public func computeLayout(
6567
proposedSize: SIMD2<Int>,
6668
environment: EnvironmentValues,
6769
dryRun: Bool = false
68-
) -> ViewUpdateResult {
69-
update(proposedSize, environment, dryRun)
70+
) -> ViewLayoutResult {
71+
computeLayout(proposedSize, environment)
72+
}
73+
74+
public func commit() -> ViewLayoutResult {
75+
_commit()
7076
}
7177
}
7278

@@ -75,61 +81,43 @@ public enum LayoutSystem {
7581
/// if all of its children have it set to true. This allows views such as
7682
/// ``Group`` to avoid changing stack layout participation (since ``Group``
7783
/// is meant to appear completely invisible to the layout system).
78-
public static func updateStackLayout<Backend: AppBackend>(
84+
public static func computeStackLayout<Backend: AppBackend>(
7985
container: Backend.Widget,
8086
children: [LayoutableChild],
8187
proposedSize: SIMD2<Int>,
8288
environment: EnvironmentValues,
8389
backend: Backend,
84-
dryRun: Bool,
8590
inheritStackLayoutParticipation: Bool = false
86-
) -> ViewUpdateResult {
91+
) -> ViewLayoutResult {
8792
let spacing = environment.layoutSpacing
88-
let alignment = environment.layoutAlignment
8993
let orientation = environment.layoutOrientation
9094

91-
var renderedChildren: [ViewUpdateResult] = Array(
92-
repeating: ViewUpdateResult.leafView(size: .empty),
95+
var renderedChildren: [ViewLayoutResult] = Array(
96+
repeating: ViewLayoutResult.leafView(size: .empty),
9397
count: children.count
9498
)
9599

96-
// Figure out which views to treat as hidden. This could be the cause
97-
// of issues if a view has some threshold at which it suddenly becomes
98-
// invisible.
100+
// My thanks go to this great article for investigating and explaining
101+
// how SwiftUI determines child view 'flexibility':
102+
// https://www.objc.io/blog/2020/11/10/hstacks-child-ordering/
99103
var isHidden = [Bool](repeating: false, count: children.count)
100-
for (i, child) in children.enumerated() {
101-
let result = child.update(
104+
let flexibilities = children.enumerated().map { i, child in
105+
let result = child.computeLayout(
102106
proposedSize: proposedSize,
103-
environment: environment,
104-
dryRun: true
107+
environment: environment
105108
)
106109
isHidden[i] = !result.participatesInStackLayouts
107-
}
108-
109-
// My thanks go to this great article for investigating and explaining
110-
// how SwiftUI determines child view 'flexibility':
111-
// https://www.objc.io/blog/2020/11/10/hstacks-child-ordering/
112-
let visibleChildrenCount = isHidden.filter { hidden in
113-
!hidden
114-
}.count
115-
let totalSpacing = max(visibleChildrenCount - 1, 0) * spacing
116-
let proposedSizeWithoutSpacing = SIMD2(
117-
proposedSize.x - (orientation == .horizontal ? totalSpacing : 0),
118-
proposedSize.y - (orientation == .vertical ? totalSpacing : 0)
119-
)
120-
let flexibilities = children.map { child in
121-
let size = child.update(
122-
proposedSize: proposedSizeWithoutSpacing,
123-
environment: environment,
124-
dryRun: true
125-
).size
126110
return switch orientation {
127111
case .horizontal:
128-
size.maximumWidth - Double(size.minimumWidth)
112+
result.size.maximumWidth - Double(result.size.minimumWidth)
129113
case .vertical:
130-
size.maximumHeight - Double(size.minimumHeight)
114+
result.size.maximumHeight - Double(result.size.minimumHeight)
131115
}
132116
}
117+
let visibleChildrenCount = isHidden.filter { hidden in
118+
!hidden
119+
}.count
120+
let totalSpacing = max(visibleChildrenCount - 1, 0) * spacing
133121
let sortedChildren = zip(children.enumerated(), flexibilities)
134122
.sorted { first, second in
135123
first.1 <= second.1
@@ -144,10 +132,9 @@ public enum LayoutSystem {
144132
// Update child in case it has just changed from visible to hidden,
145133
// and to make sure that the view is still hidden (if it's not then
146134
// it's a bug with either the view or the layout system).
147-
let result = child.update(
135+
let result = child.computeLayout(
148136
proposedSize: .zero,
149-
environment: environment,
150-
dryRun: dryRun
137+
environment: environment
151138
)
152139
if result.participatesInStackLayouts {
153140
print(
@@ -177,13 +164,12 @@ public enum LayoutSystem {
177164
proposedWidth = Double(proposedSize.x)
178165
}
179166

180-
let childResult = child.update(
167+
let childResult = child.computeLayout(
181168
proposedSize: SIMD2<Int>(
182169
Int(proposedWidth.rounded(.towardZero)),
183170
Int(proposedHeight.rounded(.towardZero))
184171
),
185-
environment: environment,
186-
dryRun: dryRun
172+
environment: environment
187173
)
188174

189175
renderedChildren[index] = childResult
@@ -247,53 +233,13 @@ public enum LayoutSystem {
247233
+ totalSpacing
248234
}
249235

250-
if !dryRun {
251-
backend.setSize(of: container, to: size)
252-
253-
var x = 0
254-
var y = 0
255-
for (index, childSize) in renderedChildren.enumerated() {
256-
// Avoid the whole iteration if the child is hidden. If there
257-
// are weird positioning issues for views that do strange things
258-
// then this could be the cause.
259-
if isHidden[index] {
260-
continue
261-
}
262-
263-
// Compute alignment
264-
switch (orientation, alignment) {
265-
case (.vertical, .leading):
266-
x = 0
267-
case (.horizontal, .leading):
268-
y = 0
269-
case (.vertical, .center):
270-
x = (size.x - childSize.size.size.x) / 2
271-
case (.horizontal, .center):
272-
y = (size.y - childSize.size.size.y) / 2
273-
case (.vertical, .trailing):
274-
x = (size.x - childSize.size.size.x)
275-
case (.horizontal, .trailing):
276-
y = (size.y - childSize.size.size.y)
277-
}
278-
279-
backend.setPosition(ofChildAt: index, in: container, to: SIMD2<Int>(x, y))
280-
281-
switch orientation {
282-
case .horizontal:
283-
x += childSize.size.size.x + spacing
284-
case .vertical:
285-
y += childSize.size.size.y + spacing
286-
}
287-
}
288-
}
289-
290236
// If the stack has been told to inherit its stack layout participation
291237
// and all of its children are hidden, then the stack itself also
292238
// shouldn't participate in stack layouts.
293239
let shouldGetIgnoredInStackLayouts =
294240
inheritStackLayoutParticipation && isHidden.allSatisfy { $0 }
295241

296-
return ViewUpdateResult(
242+
return ViewLayoutResult(
297243
size: ViewSize(
298244
size: size,
299245
idealSize: idealSize,
@@ -308,4 +254,57 @@ public enum LayoutSystem {
308254
childResults: renderedChildren
309255
)
310256
}
257+
258+
public static func commitStackLayout<Backend: AppBackend>(
259+
container: Backend.Widget,
260+
children: [LayoutableChild],
261+
layout: ViewLayoutResult,
262+
environment: EnvironmentValues,
263+
backend: Backend
264+
) {
265+
let size = layout.size.size
266+
backend.setSize(of: container, to: size)
267+
268+
let renderedChildren = children.map { $0.commit() }
269+
270+
let alignment = environment.layoutAlignment
271+
let spacing = environment.layoutSpacing
272+
let orientation = environment.layoutOrientation
273+
274+
var x = 0
275+
var y = 0
276+
for (index, child) in renderedChildren.enumerated() {
277+
// Avoid the whole iteration if the child is hidden. If there
278+
// are weird positioning issues for views that do strange things
279+
// then this could be the cause.
280+
if !child.participatesInStackLayouts {
281+
continue
282+
}
283+
284+
// Compute alignment
285+
switch (orientation, alignment) {
286+
case (.vertical, .leading):
287+
x = 0
288+
case (.horizontal, .leading):
289+
y = 0
290+
case (.vertical, .center):
291+
x = (size.x - child.size.size.x) / 2
292+
case (.horizontal, .center):
293+
y = (size.y - child.size.size.y) / 2
294+
case (.vertical, .trailing):
295+
x = (size.x - child.size.size.x)
296+
case (.horizontal, .trailing):
297+
y = (size.y - child.size.size.y)
298+
}
299+
300+
backend.setPosition(ofChildAt: index, in: container, to: SIMD2<Int>(x, y))
301+
302+
switch orientation {
303+
case .horizontal:
304+
x += child.size.size.x + spacing
305+
case .vertical:
306+
y += child.size.size.y + spacing
307+
}
308+
}
309+
}
311310
}

Sources/SwiftCrossUI/Layout/ViewUpdateResult.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public struct ViewUpdateResult {
1+
public struct ViewLayoutResult {
22
public var size: ViewSize
33
public var preferences: PreferenceValues
44

@@ -12,7 +12,7 @@ public struct ViewUpdateResult {
1212

1313
public init(
1414
size: ViewSize,
15-
childResults: [ViewUpdateResult],
15+
childResults: [ViewLayoutResult],
1616
preferencesOverlay: PreferenceValues? = nil
1717
) {
1818
self.size = size
@@ -24,7 +24,7 @@ public struct ViewUpdateResult {
2424
}
2525

2626
public static func leafView(size: ViewSize) -> Self {
27-
ViewUpdateResult(size: size, preferences: .default)
27+
ViewLayoutResult(size: size, preferences: .default)
2828
}
2929

3030
public var participatesInStackLayouts: Bool {

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
102102
backend: Backend,
103103
environment: EnvironmentValues,
104104
windowSizeIsFinal: Bool = false
105-
) -> ViewUpdateResult {
105+
) -> ViewLayoutResult {
106106
guard let window = window as? Backend.Window else {
107107
fatalError("Scene updated with a backend incompatible with the window it was given")
108108
}
@@ -136,7 +136,7 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
136136
}
137137
.with(\.window, window)
138138

139-
let dryRunResult: ViewUpdateResult?
139+
let dryRunResult: ViewLayoutResult?
140140
if !windowSizeIsFinal {
141141
// Perform a dry-run update of the root view to check if the window
142142
// needs to change size.

0 commit comments

Comments
 (0)