Skip to content

Commit 3638080

Browse files
committed
Add lazy vertical and horizontal stacks
1 parent 5a2e23f commit 3638080

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// LazyStacks.swift
3+
// AndroidSwiftUICore
4+
//
5+
// Genuinely lazy stacks. Unlike `List`, which takes its data directly, a lazy
6+
// stack takes a ViewBuilder — so the laziness has to come from the `ForEach`
7+
// inside it. When the content is exactly one `ForEach`, its elements are
8+
// exposed through an item provider and resolved on demand; anything else
9+
// (mixed or static content) falls back to resolving children eagerly, which
10+
// is correct, just not lazy.
11+
//
12+
13+
/// Type-erased access to a `ForEach`'s elements, so a lazy container can
14+
/// resolve one element at a time instead of flattening them all.
15+
internal protocol _LazyElementProvider {
16+
var _elementCount: Int { get }
17+
func _elementKey(at index: Int) -> String
18+
func _resolveElement(at index: Int, in context: ResolveContext) -> RenderNode
19+
}
20+
21+
extension ForEach: _LazyElementProvider {
22+
23+
internal var _elementCount: Int { data.count }
24+
25+
private func element(at index: Int) -> Data.Element {
26+
data[data.index(data.startIndex, offsetBy: index)]
27+
}
28+
29+
internal func _elementKey(at index: Int) -> String {
30+
identityString(id(element(at: index)))
31+
}
32+
33+
internal func _resolveElement(at index: Int, in context: ResolveContext) -> RenderNode {
34+
let element = self.element(at: index)
35+
// keyed by identity, matching the eager path, so element @State survives
36+
// scrolling and reordering
37+
let key = "#\(identityString(id(element)))"
38+
return Evaluator.resolve(content(element), context.descending(key))
39+
}
40+
}
41+
42+
/// Builds a lazy stack node, using the item-provider path when the content is a
43+
/// single `ForEach` and falling back to eager children otherwise.
44+
internal func _lazyStackNode<Content: View>(
45+
type: String,
46+
content: Content,
47+
props: [String: PropValue],
48+
context: ResolveContext
49+
) -> RenderNode {
50+
var props = props
51+
guard let provider = content as? _LazyElementProvider else {
52+
return RenderNode(
53+
type: type,
54+
id: context.path,
55+
props: props,
56+
children: Evaluator.resolveChildren(content, context.descending("content"))
57+
)
58+
}
59+
let storage = context.storage
60+
let callbacks = context.callbacks
61+
let environment = context.environment
62+
let basePath = context.path + "/content"
63+
let count = provider._elementCount
64+
65+
let providerID = callbacks.register(.item { index in
66+
let elementContext = ResolveContext(
67+
storage: storage,
68+
callbacks: callbacks,
69+
environment: environment,
70+
path: basePath
71+
)
72+
return provider._resolveElement(at: index, in: elementContext)
73+
})
74+
props["itemProvider"] = .int(Int(providerID))
75+
props["keys"] = .array((0..<count).map { .string(provider._elementKey(at: $0)) })
76+
return RenderNode(type: type, id: context.path, props: props, count: count)
77+
}
78+
79+
/// A vertically stacked layout that only resolves the elements in view.
80+
public struct LazyVStack<Content: View>: View {
81+
82+
internal let alignment: HorizontalAlignment
83+
internal let spacing: Double?
84+
internal let content: Content
85+
86+
public init(
87+
alignment: HorizontalAlignment = .center,
88+
spacing: Double? = nil,
89+
@ViewBuilder content: () -> Content
90+
) {
91+
self.alignment = alignment
92+
self.spacing = spacing
93+
self.content = content()
94+
}
95+
96+
public typealias Body = Never
97+
}
98+
99+
extension LazyVStack: PrimitiveView {
100+
public func _render(in context: ResolveContext) -> RenderNode {
101+
var props: [String: PropValue] = ["alignment": .string(alignment.rawValue)]
102+
if let spacing { props["spacing"] = .double(spacing) }
103+
return _lazyStackNode(type: "LazyVStack", content: content, props: props, context: context)
104+
}
105+
}
106+
107+
/// A horizontally stacked layout that only resolves the elements in view.
108+
public struct LazyHStack<Content: View>: View {
109+
110+
internal let alignment: VerticalAlignment
111+
internal let spacing: Double?
112+
internal let content: Content
113+
114+
public init(
115+
alignment: VerticalAlignment = .center,
116+
spacing: Double? = nil,
117+
@ViewBuilder content: () -> Content
118+
) {
119+
self.alignment = alignment
120+
self.spacing = spacing
121+
self.content = content()
122+
}
123+
124+
public typealias Body = Never
125+
}
126+
127+
extension LazyHStack: PrimitiveView {
128+
public func _render(in context: ResolveContext) -> RenderNode {
129+
var props: [String: PropValue] = ["alignment": .string(alignment.rawValue)]
130+
if let spacing { props["spacing"] = .double(spacing) }
131+
return _lazyStackNode(type: "LazyHStack", content: content, props: props, context: context)
132+
}
133+
}

0 commit comments

Comments
 (0)