From 0d91ae5ae212519b9a72dc8e369e9dc2d0047ed2 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Tue, 20 Jan 2026 10:04:12 -0800 Subject: [PATCH] Display multiple elements in section headers We're adding a new environment variable so the grid/stack/list can convey an axis to the `Section`, so it knows whether to make a VStack or an HStack. Fixes #311 --- .../SkipUI/SkipUI/Containers/LazyHGrid.swift | 7 +++++- .../SkipUI/SkipUI/Containers/LazyHStack.swift | 7 +++++- .../SkipUI/SkipUI/Containers/LazyVGrid.swift | 7 +++++- .../SkipUI/SkipUI/Containers/LazyVStack.swift | 7 +++++- Sources/SkipUI/SkipUI/Containers/List.swift | 20 +++++++++------- .../SkipUI/SkipUI/Containers/Section.swift | 23 +++++++++++++++++-- .../Environment/EnvironmentValues.swift | 7 ++++++ 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift b/Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift index 22e464bf..e15b8acd 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift @@ -61,7 +61,12 @@ public struct LazyHGrid: View, Renderable { let scrollAxes: Axis.Set = isScrollEnabled ? Axis.Set.horizontal : [] let scrollTargetBehavior = EnvironmentValues.shared._scrollTargetBehavior - let renderables = content.EvaluateLazyItems(level: 0, context: context) + let renderables = EnvironmentValues.shared.setValuesWithReturn({ + $0.set_lazySectionStackAxis(Axis.horizontal) + return ComposeResult.ok + }, in: { + content.EvaluateLazyItems(level: 0, context: context) + }) let itemContext = context.content() let itemCollector = remember { mutableStateOf(LazyItemCollector()) } ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in diff --git a/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift b/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift index 1f2a5775..5926da20 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift @@ -53,7 +53,12 @@ public struct LazyHStack : View, Renderable { let scrollAxes: Axis.Set = isScrollEnabled ? Axis.Set.horizontal : [] let scrollTargetBehavior = EnvironmentValues.shared._scrollTargetBehavior - let renderables = content.EvaluateLazyItems(level: 0, context: context) + let renderables = EnvironmentValues.shared.setValuesWithReturn({ + $0.set_lazySectionStackAxis(Axis.horizontal) + return ComposeResult.ok + }, in: { + content.EvaluateLazyItems(level: 0, context: context) + }) let itemContext = context.content() let itemCollector = remember { mutableStateOf(LazyItemCollector()) } ComposeContainer(axis: .horizontal, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift index 73417cf7..e5bc057f 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift @@ -68,7 +68,12 @@ public struct LazyVGrid: View, Renderable { let searchableState = EnvironmentValues.shared._searchableState let isSearchable = searchableState?.isOnNavigationStack == false - let renderables = content.EvaluateLazyItems(level: 0, context: context) + let renderables = EnvironmentValues.shared.setValuesWithReturn({ + $0.set_lazySectionStackAxis(Axis.vertical) + return ComposeResult.ok + }, in: { + content.EvaluateLazyItems(level: 0, context: context) + }) let itemContext = context.content() let itemCollector = remember { mutableStateOf(LazyItemCollector()) } ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift index 7c30961d..f24523d5 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift @@ -61,7 +61,12 @@ public struct LazyVStack : View, Renderable { let searchableState = EnvironmentValues.shared._searchableState let isSearchable = searchableState?.isOnNavigationStack == false - let renderables = content.EvaluateLazyItems(level: 0, context: context) + let renderables = EnvironmentValues.shared.setValuesWithReturn({ + $0.set_lazySectionStackAxis(Axis.vertical) + return ComposeResult.ok + }, in: { + content.EvaluateLazyItems(level: 0, context: context) + }) let itemContext = context.content() let itemCollector = remember { mutableStateOf(LazyItemCollector()) } ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true) { modifier in diff --git a/Sources/SkipUI/SkipUI/Containers/List.swift b/Sources/SkipUI/SkipUI/Containers/List.swift index f51bbb7e..d23533f8 100644 --- a/Sources/SkipUI/SkipUI/Containers/List.swift +++ b/Sources/SkipUI/SkipUI/Containers/List.swift @@ -157,14 +157,18 @@ public final class List : View, Renderable { } @Composable private func RenderList(context: ComposeContext, styling: ListStyling, arguments: ListArguments) { - let renderables: kotlin.collections.List - if let forEach { - renderables = forEach.EvaluateLazyItems(level: 0, context: context) - } else if let fixedContent { - renderables = fixedContent.EvaluateLazyItems(level: 0, context: context) - } else { - renderables = listOf() - } + let renderables = EnvironmentValues.shared.setValuesWithReturn({ + $0.set_lazySectionStackAxis(Axis.vertical) + return ComposeResult.ok + }, in: { + if let forEach { + return forEach.EvaluateLazyItems(level: 0, context: context) + } else if let fixedContent { + return fixedContent.EvaluateLazyItems(level: 0, context: context) + } else { + return listOf() + } + }) var modifier = context.modifier if styling.style != .plain { diff --git a/Sources/SkipUI/SkipUI/Containers/Section.swift b/Sources/SkipUI/SkipUI/Containers/Section.swift index 6c4c0044..edd1e8ae 100644 --- a/Sources/SkipUI/SkipUI/Containers/Section.swift +++ b/Sources/SkipUI/SkipUI/Containers/Section.swift @@ -83,18 +83,37 @@ public struct Section : View { #if SKIP @Composable override func Evaluate(context: ComposeContext, options: Int) -> kotlin.collections.List { + let stackAxis = EnvironmentValues.shared._lazySectionStackAxis ?? Axis.vertical + func combined(_ list: kotlin.collections.List?, stackAxis: Axis) -> Renderable { + let filtered = (list ?? listOf()).filter { !$0.isSwiftUIEmptyView } + if filtered.size == 0 { + return EmptyView() + } + if filtered.size == 1 { + return filtered[0] + } + if stackAxis == Axis.horizontal { + return HStack(alignment: .center, spacing: 0) { + ForEach(0.. = mutableListOf() let headerRenderables = header?.Evaluate(context: context, options: 0) if isLazy { - renderables.add(LazySectionHeader(content: headerRenderables?.firstOrNull() ?? EmptyView())) + renderables.add(LazySectionHeader(content: combined(headerRenderables, stackAxis: stackAxis))) } else if let headerRenderables { renderables.addAll(headerRenderables) } renderables.addAll(content.Evaluate(context: context, options: options)) let footerRenderables = footer?.Evaluate(context: context, options: 0) if isLazy { - renderables.add(LazySectionFooter(content: footerRenderables?.firstOrNull() ?? EmptyView())) + renderables.add(LazySectionFooter(content: combined(footerRenderables, stackAxis: stackAxis))) } else if let footerRenderables { renderables.addAll(footerRenderables) } diff --git a/Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift b/Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift index 2dc3ab35..3833e106 100644 --- a/Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift +++ b/Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift @@ -728,6 +728,13 @@ extension EnvironmentValues { set { setBuiltinValue(key: "_listStyle", value: newValue, defaultValue: { nil }) } } + /// Axis for stacking multiple views in a Section header/footer when in a lazy container. + /// LazyVGrid/LazyVStack/List use .vertical (VStack); LazyHGrid/LazyHStack use .horizontal (HStack). + var _lazySectionStackAxis: Axis? { + get { builtinValue(key: "_lazySectionStackAxis", defaultValue: { nil }) as! Axis? } + set { setBuiltinValue(key: "_lazySectionStackAxis", value: newValue, defaultValue: { nil }) } + } + var _material3BottomAppBar: (@Composable (Material3BottomAppBarOptions) -> Material3BottomAppBarOptions)? { get { builtinValue(key: "_material3BottomAppBar", defaultValue: { nil }) as! (@Composable (Material3BottomAppBarOptions) -> Material3BottomAppBarOptions)? } set { setBuiltinValue(key: "_material3BottomAppBar", value: newValue, defaultValue: { nil }) }