Skip to content

Commit 498b8b3

Browse files
committed
Introduce concept of hidden views, ignore them when spacing stack children
This stops hidden views from affecting stack layouts and makes them act closer to how you'd intuitively expect. For now the only hidden views are OptionalViews with a nil wrapped view, and Groups with all children hidden. The Group rule makes it so that Groups can be introduced without affecting layouts, maximizing their usefulness.
1 parent bc5cba4 commit 498b8b3

File tree

4 files changed

+106
-14
lines changed

4 files changed

+106
-14
lines changed

Sources/SwiftCrossUI/LayoutSystem.swift

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,50 @@ public enum LayoutSystem {
2727
}
2828
}
2929

30+
/// - Parameter inheritStackLayoutParticipation: If `true`, the stack layout
31+
/// will have ``ViewSize/participateInStackLayoutsWhenEmpty`` set to `true`
32+
/// if all of its children have it set to true. This allows views such as
33+
/// ``Group`` to avoid changing stack layout participation (since ``Group``
34+
/// is meant to appear completely invisible to the layout system).
3035
public static func updateStackLayout<Backend: AppBackend>(
3136
container: Backend.Widget,
3237
children: [LayoutableChild],
3338
proposedSize: SIMD2<Int>,
3439
environment: Environment,
3540
backend: Backend,
36-
dryRun: Bool
41+
dryRun: Bool,
42+
inheritStackLayoutParticipation: Bool = false
3743
) -> ViewSize {
3844
let spacing = environment.layoutSpacing
3945
let alignment = environment.layoutAlignment
4046
let orientation = environment.layoutOrientation
41-
let totalSpacing = (children.count - 1) * spacing
42-
43-
var spaceUsedAlongStackAxis = 0
44-
var childrenRemaining = children.count
4547

4648
var renderedChildren = [ViewSize](
4749
repeating: .empty,
4850
count: children.count
4951
)
5052

51-
// My thanks go to this great article for investigating and explaining how SwiftUI determines
52-
// child view 'flexibility': https://www.objc.io/blog/2020/11/10/hstacks-child-ordering/
53+
// Figure out which views to treat as hidden. This could be the cause
54+
// of issues if a view has some threshold at which it suddenly becomes
55+
// invisible.
56+
var isHidden = [Bool](repeating: false, count: children.count)
57+
for (i, child) in children.enumerated() {
58+
let size = child.computeSize(
59+
proposedSize: proposedSize,
60+
environment: environment
61+
)
62+
isHidden[i] =
63+
size.size == .zero
64+
&& !size.participateInStackLayoutsWhenEmpty
65+
}
66+
67+
// My thanks go to this great article for investigating and explaining
68+
// how SwiftUI determines child view 'flexibility':
69+
// https://www.objc.io/blog/2020/11/10/hstacks-child-ordering/
70+
let visibleChildrenCount = isHidden.count { hidden in
71+
!hidden
72+
}
73+
let totalSpacing = (visibleChildrenCount - 1) * spacing
5374
let proposedSizeWithoutSpacing = SIMD2(
5475
proposedSize.x - (orientation == .horizontal ? totalSpacing : 0),
5576
proposedSize.y - (orientation == .vertical ? totalSpacing : 0)
@@ -66,11 +87,40 @@ public enum LayoutSystem {
6687
size.maximumHeight - Double(size.minimumHeight)
6788
}
6889
}
69-
let sortedChildren = zip(children.enumerated(), flexibilities).sorted { first, second in
70-
first.1 <= second.1
71-
}.map(\.0)
90+
let sortedChildren = zip(children.enumerated(), flexibilities)
91+
.sorted { first, second in
92+
first.1 <= second.1
93+
}
94+
.map(\.0)
7295

96+
var spaceUsedAlongStackAxis = 0
97+
var childrenRemaining = visibleChildrenCount
7398
for (index, child) in sortedChildren {
99+
// No need to render visible children.
100+
if isHidden[index] {
101+
// Update child in case it has just changed from visible to hidden,
102+
// and to make sure that the view is still hidden (if it's not then
103+
// it's a bug with either the view or the layout system).
104+
let size = child.update(
105+
proposedSize: .zero,
106+
environment: environment,
107+
dryRun: dryRun
108+
)
109+
if size.size != .zero || size.participateInStackLayoutsWhenEmpty {
110+
print("warning: Hidden view became visible on second update. Layout may break.")
111+
}
112+
renderedChildren[index] = ViewSize(
113+
size: .zero,
114+
idealSize: .zero,
115+
minimumWidth: 0,
116+
minimumHeight: 0,
117+
maximumWidth: 0,
118+
maximumHeight: 0,
119+
participateInStackLayoutsWhenEmpty: false
120+
)
121+
continue
122+
}
123+
74124
let proposedWidth: Double
75125
let proposedHeight: Double
76126
switch orientation {
@@ -149,6 +199,13 @@ public enum LayoutSystem {
149199
var x = 0
150200
var y = 0
151201
for (index, childSize) in renderedChildren.enumerated() {
202+
// Avoid the whole iteration if the child is hidden. If there
203+
// are weird positioning issues for views that do strange things
204+
// then this could be the cause.
205+
if isHidden[index] {
206+
continue
207+
}
208+
152209
// Compute alignment
153210
switch (orientation, alignment) {
154211
case (.vertical, .leading):
@@ -182,7 +239,10 @@ public enum LayoutSystem {
182239
minimumWidth: minimumWidth,
183240
minimumHeight: minimumHeight,
184241
maximumWidth: maximumWidth,
185-
maximumHeight: maximumHeight
242+
maximumHeight: maximumHeight,
243+
participateInStackLayoutsWhenEmpty:
244+
inheritStackLayoutParticipation
245+
&& isHidden.allSatisfy { $0 }
186246
)
187247
}
188248
}

Sources/SwiftCrossUI/ViewGraph/ViewSize.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ public struct ViewSize: Equatable {
1616
maximumHeight: 0
1717
)
1818

19+
/// The view update result for a hidden view. Differs from ``ViewSize/empty``
20+
/// by stopping hidden views from participating in stack layouts (i.e.
21+
/// getting spacing between the previous child and the hidden child).
22+
public static let hidden = ViewSize(
23+
size: .zero,
24+
idealSize: .zero,
25+
minimumWidth: 0,
26+
minimumHeight: 0,
27+
maximumWidth: 0,
28+
maximumHeight: 0,
29+
participateInStackLayoutsWhenEmpty: false
30+
)
31+
1932
/// The size that the view now takes up.
2033
public var size: SIMD2<Int>
2134
/// The size that the view ideally wants to take up.
@@ -28,14 +41,29 @@ public struct ViewSize: Equatable {
2841
public var maximumWidth: Double
2942
/// The maximum height that the view can take (if its width remains the same).
3043
public var maximumHeight: Double
44+
/// Whether the view should participate in stack layouts when empty.
45+
///
46+
/// If `false`, the view won't get any spacing before or after it in stack
47+
/// layouts. For example, this is used by ``OptionalView`` when its
48+
/// underlying view is `nil` to avoid having spacing between views that are
49+
/// semantically 'not present'.
50+
///
51+
/// Only takes effect when ``ViewSize/size`` is zero, to avoid any ambiguity
52+
/// when the view has non-zero size as this option is really only intended
53+
/// to be used for visually hidden views (what would it mean for a non-empty
54+
/// view to not participate in the layout? would the spacing between the
55+
/// previous view and the next go before or after the view? would the view
56+
/// get forced to zero size?).
57+
public var participateInStackLayoutsWhenEmpty: Bool
3158

3259
public init(
3360
size: SIMD2<Int>,
3461
idealSize: SIMD2<Int>,
3562
minimumWidth: Int,
3663
minimumHeight: Int,
3764
maximumWidth: Double?,
38-
maximumHeight: Double?
65+
maximumHeight: Double?,
66+
participateInStackLayoutsWhenEmpty: Bool = true
3967
) {
4068
self.size = size
4169
self.idealSize = idealSize
@@ -52,6 +80,8 @@ public struct ViewSize: Equatable {
5280
// I believe is a good compromise.
5381
self.maximumWidth = maximumWidth ?? Double(1 << 53)
5482
self.maximumHeight = maximumHeight ?? Double(1 << 53)
83+
self.participateInStackLayoutsWhenEmpty =
84+
participateInStackLayoutsWhenEmpty
5585
}
5686

5787
public init(fixedSize: SIMD2<Int>) {
@@ -61,5 +91,6 @@ public struct ViewSize: Equatable {
6191
minimumHeight = fixedSize.y
6292
maximumWidth = Double(fixedSize.x)
6393
maximumHeight = Double(fixedSize.y)
94+
participateInStackLayoutsWhenEmpty = true
6495
}
6596
}

Sources/SwiftCrossUI/Views/Group.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public struct Group<Content: View>: View {
3737
proposedSize: proposedSize,
3838
environment: environment,
3939
backend: backend,
40-
dryRun: dryRun
40+
dryRun: dryRun,
41+
inheritStackLayoutParticipation: true
4142
)
4243
return size
4344
}

Sources/SwiftCrossUI/Views/OptionalView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public struct OptionalView<V: View>: TypeSafeView, View {
7171
} else {
7272
hasToggled = children.node != nil
7373
children.node = nil
74-
size = .empty
74+
size = .hidden
7575
}
7676
children.hasToggled = children.hasToggled || hasToggled
7777

0 commit comments

Comments
 (0)