From e9cf4ed182e53a7f228c7d63479b2552b1ccd92d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 08:10:19 -0400 Subject: [PATCH 1/5] Add Form and Section --- .../AndroidSwiftUICore/Primitives/Form.swift | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Form.swift diff --git a/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Form.swift b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Form.swift new file mode 100644 index 0000000..e7634b2 --- /dev/null +++ b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Form.swift @@ -0,0 +1,65 @@ +// +// Form.swift +// AndroidSwiftUICore +// +// A Form is a grouped, scrolling container of Sections; a Section groups rows +// under an optional header and footer. The interpreter renders sections as +// inset, rounded groups with dividers between rows. +// + +public struct Form: View { + internal let content: Content + public init(@ViewBuilder content: () -> Content) { self.content = content() } + public typealias Body = Never +} + +extension Form: PrimitiveView { + public func _render(in context: ResolveContext) -> RenderNode { + RenderNode( + type: "Form", + id: context.path, + children: Evaluator.resolveChildren(content, context.descending("content")) + ) + } +} + +public struct Section: View { + + internal let header: String? + internal let footer: String? + internal let content: Content + + public init(@ViewBuilder content: () -> Content) { + self.header = nil + self.footer = nil + self.content = content() + } + + public init(_ header: S, @ViewBuilder content: () -> Content) { + self.header = String(header) + self.footer = nil + self.content = content() + } + + public init(header: H, footer: F, @ViewBuilder content: () -> Content) { + self.header = String(header) + self.footer = String(footer) + self.content = content() + } + + public typealias Body = Never +} + +extension Section: PrimitiveView { + public func _render(in context: ResolveContext) -> RenderNode { + var props: [String: PropValue] = [:] + if let header { props["header"] = .string(header) } + if let footer { props["footer"] = .string(footer) } + return RenderNode( + type: "Section", + id: context.path, + props: props, + children: Evaluator.resolveChildren(content, context.descending("content")) + ) + } +} From 7b0ebf11e90f090a01fc72b43abf4a888f86abde Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 08:10:19 -0400 Subject: [PATCH 2/5] Test Form and Section nesting --- .../AndroidSwiftUICoreTests/ControlTests.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/ControlTests.swift b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/ControlTests.swift index 5e4d920..292dfc6 100644 --- a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/ControlTests.swift +++ b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/ControlTests.swift @@ -115,6 +115,23 @@ struct ControlTests { #expect(node.children.count == 2) } + @Test("Form nests Sections with their header and rows") + func formSection() { + let node = ViewHost( + Form { + Section("General") { + Text("Wi-Fi") + Text("Bluetooth") + } + } + ).evaluate() + #expect(node.type == "Form") + let section = node.children.first + #expect(section?.type == "Section") + #expect(section?.props["header"] == .string("General")) + #expect(section?.children.count == 2) + } + @Test("Environment objects reach @Environment properties in the subtree") func environmentInjection() { final class Model { var value = 42 } From e5f1230821323d330f1eb264f39a3122748ed35f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 08:10:19 -0400 Subject: [PATCH 3/5] Render Form and Section as grouped lists --- .../kotlin/com/pureswift/swiftui/Render.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt index 7f3be00..83e3802 100644 --- a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt @@ -34,6 +34,8 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -240,6 +242,10 @@ fun Render(node: ViewNode) { "TabView" -> RenderTabView(node) + "Form" -> RenderForm(node) + + "Section" -> RenderSection(node) + "List" -> RenderList(node) "LazyVGrid" -> RenderGrid(node, vertical = true) @@ -313,6 +319,51 @@ private fun RenderStepper(node: ViewNode) { } } +// A Form scrolls its Sections; non-Section children still render, ungrouped. +@Composable +private fun RenderForm(node: ViewNode) { + val state = rememberScrollState() + Column(modifier = node.composeModifiers().fillMaxSize().verticalScroll(state).padding(vertical = 8.dp)) { + RenderChildren(node) + } +} + +// An inset, rounded group: uppercase header, rows separated by dividers, footer. +@Composable +private fun RenderSection(node: ViewNode) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { + node.string("header")?.let { + Text( + it.uppercase(), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 4.dp, bottom = 6.dp), + ) + } + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(10.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column { + val rows = node.children.filter { !it.isPresentation() } + rows.forEachIndexed { index, row -> + if (index > 0) HorizontalDivider() + Box(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { Render(row) } + } + } + } + node.string("footer")?.let { + Text( + it, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 4.dp, top = 6.dp), + ) + } + } +} + // A trigger button opening a dropdown; each child Button becomes a menu item // firing its own tap callback. @Composable From 018b64649b5556f9e81a22034f6b1c08d7d4a59f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 08:10:19 -0400 Subject: [PATCH 4/5] Add form playground --- .../App.swiftpm/Sources/FormPlaygrounds.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/FormPlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/FormPlaygrounds.swift b/Demo/App.swiftpm/Sources/FormPlaygrounds.swift new file mode 100644 index 0000000..1a78c19 --- /dev/null +++ b/Demo/App.swiftpm/Sources/FormPlaygrounds.swift @@ -0,0 +1,28 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +struct FormPlayground: View { + @State private var wifi = true + @State private var bluetooth = false + @State private var brightness = 0.6 + @State private var name = "" + var body: some View { + Form { + Section("Connectivity") { + Toggle("Wi-Fi", isOn: $wifi) + Toggle("Bluetooth", isOn: $bluetooth) + } + Section(header: "Display", footer: "Drag to adjust the screen brightness.") { + Slider(value: $brightness, in: 0...1) + HStack { Text("Level"); Spacer(); Text("\(Int(brightness * 100))%") } + } + Section("Account") { + TextField("Name", text: $name) + HStack { Text("Status"); Spacer(); Text(name.isEmpty ? "Guest" : name) } + } + } + } +} From fbd47cd78d9f90e9fc3774636ac770a0bc6b5838 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 08:10:19 -0400 Subject: [PATCH 5/5] Add Form to the catalog --- Demo/App.swiftpm/Sources/Catalog.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Demo/App.swiftpm/Sources/Catalog.swift b/Demo/App.swiftpm/Sources/Catalog.swift index 81efe91..d4ee1fe 100644 --- a/Demo/App.swiftpm/Sources/Catalog.swift +++ b/Demo/App.swiftpm/Sources/Catalog.swift @@ -39,6 +39,7 @@ struct CatalogEntry: Identifiable { CatalogEntry(id: "scroll", title: "ScrollView", screen: AnyCatalogScreen(ScrollViewPlayground())), CatalogEntry(id: "list", title: "List", screen: AnyCatalogScreen(ListPlayground())), CatalogEntry(id: "grid", title: "Grid", screen: AnyCatalogScreen(GridPlayground())), + CatalogEntry(id: "form", title: "Form", screen: AnyCatalogScreen(FormPlayground())), CatalogEntry(id: "modifier", title: "Modifiers", screen: AnyCatalogScreen(ModifierPlayground())), CatalogEntry(id: "interaction", title: "Interaction", screen: AnyCatalogScreen(InteractionPlayground())), CatalogEntry(id: "navigation", title: "Navigation", screen: AnyCatalogScreen(NavigationPlayground())),