From 66cb50ee6e5a2dc07c628b4e6ddce07fdc713d69 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:23:16 -0400 Subject: [PATCH 01/20] Render the gallery catalog in the desktop rig Ports the gallery screens against the core and drives the desktop window from ContentView, with a main-thread scheduler for async state. --- Sources/SwiftUIDesktopDemo/DesktopDemo.swift | 20 ++--- .../Gallery/ButtonScreen.swift | 26 +++++++ .../Gallery/ContentView.swift | 29 ++++++++ .../Gallery/ControlsScreen.swift | 56 ++++++++++++++ .../Gallery/GridScreen.swift | 37 ++++++++++ .../Gallery/ListScreen.swift | 32 ++++++++ .../Gallery/ModifierScreen.swift | 68 +++++++++++++++++ .../Gallery/NavigationScreen.swift | 73 +++++++++++++++++++ .../Gallery/ObservationScreen.swift | 37 ++++++++++ .../Gallery/SheetScreen.swift | 46 ++++++++++++ .../Gallery/StacksScreen.swift | 69 ++++++++++++++++++ .../Gallery/StateScreen.swift | 46 ++++++++++++ .../Gallery/TabScreen.swift | 27 +++++++ .../Gallery/TextScreen.swift | 21 ++++++ 14 files changed, 573 insertions(+), 14 deletions(-) create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ButtonScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ContentView.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ControlsScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/GridScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ListScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ModifierScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/NavigationScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/ObservationScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/SheetScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/StacksScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/StateScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/TabScreen.swift create mode 100644 Sources/SwiftUIDesktopDemo/Gallery/TextScreen.swift diff --git a/Sources/SwiftUIDesktopDemo/DesktopDemo.swift b/Sources/SwiftUIDesktopDemo/DesktopDemo.swift index e43df5f..fe71743 100644 --- a/Sources/SwiftUIDesktopDemo/DesktopDemo.swift +++ b/Sources/SwiftUIDesktopDemo/DesktopDemo.swift @@ -6,23 +6,11 @@ // pushed through the bridge into the rig's Compose window. // +import Foundation import AndroidSwiftUICore import AndroidSwiftUIBridge import SwiftJava -struct CounterDemo: View { - - @State private var count = 0 - - var body: some View { - VStack(spacing: 12) { - Text("Count: \(count)") - Button("Increment") { count += 1 } - Toggle("Feature flag", isOn: .constant(true)) - } - } -} - @JavaClass("com.pureswift.swiftui.desktop.SwiftRuntime") open class SwiftRuntime: JavaObject { } @@ -33,7 +21,11 @@ extension SwiftRuntime { @JavaMethod func start(_ store: TreeStore?) { guard let store else { return } - let runtime = BridgeRuntime(root: CounterDemo(), store: store) + // marshal re-renders onto the main thread; async state (e.g. a List + // refresh Task) writes off-thread and JNI object creation must run here + let runtime = BridgeRuntime(root: ContentView(), store: store) { block in + DispatchQueue.main.async { block() } + } runtime.start() } } diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ButtonScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/ButtonScreen.swift new file mode 100644 index 0000000..42d9837 --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ButtonScreen.swift @@ -0,0 +1,26 @@ +import AndroidSwiftUICore + +/// Button initializers and actions. +struct ButtonScreen: View { + + @State + private var tapCount = 0 + + var body: some View { + VStack(spacing: 16) { + Text(verbatim: "Taps: \(tapCount)") + Button("Title initializer") { + tapCount += 1 + } + Button(action: { tapCount += 1 }) { + Text("Label closure initializer") + } + Button(action: { tapCount += 1 }) { + Image("globe") + } + Button("Reset") { + tapCount = 0 + } + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ContentView.swift b/Sources/SwiftUIDesktopDemo/Gallery/ContentView.swift new file mode 100644 index 0000000..c61fbdc --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ContentView.swift @@ -0,0 +1,29 @@ +import AndroidSwiftUICore + +/// Gallery of playground screens, navigated with a `NavigationStack`. Each row +/// pushes a feature screen; `List`-based navigation returns with lazy +/// containers in the next step. +struct ContentView: View { + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 0) { + NavigationLink("Text", destination: TextScreen()) + NavigationLink("Buttons", destination: ButtonScreen()) + NavigationLink("Stacks", destination: StacksScreen()) + NavigationLink("List", destination: ListScreen()) + NavigationLink("Grid", destination: GridScreen()) + NavigationLink("State", destination: StateScreen()) + NavigationLink("Controls", destination: ControlsScreen()) + NavigationLink("Modifiers", destination: ModifierScreen()) + NavigationLink("Observation", destination: ObservationScreen()) + NavigationLink("Navigation", destination: NavigationScreen()) + NavigationLink("Sheets", destination: SheetScreen()) + NavigationLink("Tabs", destination: TabScreen()) + } + } + .navigationTitle("Gallery") + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ControlsScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/ControlsScreen.swift new file mode 100644 index 0000000..9a93bec --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ControlsScreen.swift @@ -0,0 +1,56 @@ +import AndroidSwiftUICore + +/// Form controls and their state bindings. +struct ControlsScreen: View { + + @State + private var isOn = false + + @State + private var progress = 0.25 + + @State + private var sliderValue = 0.5 + + @State + private var name = "" + + @State + private var fruit = "Apple" + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Toggle") + Toggle("Enabled", isOn: $isOn) + Text(isOn ? "Toggle is on" : "Toggle is off") + Divider() + Text("Indeterminate progress") + ProgressView() + Divider() + Text("Determinate progress") + ProgressView(value: progress) + Text("\(Int(progress * 100))%") + Button("Advance") { + progress = progress >= 1 ? 0 : progress + 0.25 + } + Divider() + Text("Slider") + Slider(value: $sliderValue, in: 0...1) + Text("Value: \(Int(sliderValue * 100))%") + Divider() + Text("Text field") + TextField("Name", text: $name) + Text(name.isEmpty ? "Nothing typed yet" : "Hello, \(name)") + Divider() + Text("Picker") + Picker("Fruit", selection: $fruit) { + Text("Apple").tag("Apple") + Text("Banana").tag("Banana") + Text("Cherry").tag("Cherry") + } + Text("Selected: \(fruit)") + } + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/GridScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/GridScreen.swift new file mode 100644 index 0000000..c941409 --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/GridScreen.swift @@ -0,0 +1,37 @@ +import AndroidSwiftUICore + +/// Lazy grids. +struct GridScreen: View { + + var body: some View { + VStack(spacing: 16) { + Text("Three fixed columns") + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]) { + ForEach(1...9, id: \.self) { number in + Text("\(number)") + .padding() + .background(Color.blue) + } + } + Divider() + Text("Adaptive columns") + LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { + ForEach(1...8, id: \.self) { number in + Text("Item \(number)") + .padding() + .background(Color.orange) + } + } + Divider() + Text("Two rows, horizontal") + LazyHGrid(rows: [GridItem(.flexible()), GridItem(.flexible())]) { + ForEach(1...8, id: \.self) { number in + Text("Cell \(number)") + .padding() + .background(Color.green) + } + } + .frame(height: 160) + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ListScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/ListScreen.swift new file mode 100644 index 0000000..d16854c --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ListScreen.swift @@ -0,0 +1,32 @@ +import AndroidSwiftUICore + +/// A lazy list with pull to refresh. Rows are evaluated on demand, so the +/// large-list variant scrolls without materializing every row up front. +struct ListScreen: View { + + @State + private var items = (1...30).map(ListItem.init) + + var body: some View { + List(items) { item in + Text(item.title) + } + .refreshable { + await addItem() + } + } + + private func addItem() async { + try? await Task.sleep(for: .seconds(1)) + items.insert(ListItem(id: items.count + 1), at: 0) + } +} + +struct ListItem: Identifiable { + + let id: Int + + var title: String { + "Row \(id)" + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ModifierScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/ModifierScreen.swift new file mode 100644 index 0000000..8733e0d --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ModifierScreen.swift @@ -0,0 +1,68 @@ +import AndroidSwiftUICore + +/// Layout and effect modifiers. +struct ModifierScreen: View { + + var body: some View { + ScrollView { + VStack(spacing: 24) { + ModifierSection(title: "Padding") { + Text("Padded") + .padding() + .background(Color.blue) + } + ModifierSection(title: "Frame") { + Text("Fixed 200x60") + .frame(width: 200, height: 60) + .background(Color.green) + } + ModifierSection(title: "Background") { + Text("Colored background") + .padding() + .background(Color.orange) + } + ModifierSection(title: "Clip + corner radius") { + Text("Rounded") + .padding() + .background(Color.purple) + .cornerRadius(16) + } + ModifierSection(title: "Offset") { + Text("Shifted") + .offset(x: 30, y: 0) + .background(Color.red) + } + ModifierSection(title: "Rotation") { + Text("Rotated") + .background(Color.yellow) + .rotationEffect(.degrees(15)) + } + ModifierSection(title: "Scale") { + Text("Scaled") + .background(Color.pink) + .scaleEffect(1.5) + } + } + } + } +} + +struct ModifierSection: View { + + let title: String + + let content: Content + + init(title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + + var body: some View { + VStack(spacing: 8) { + Text(title) + content + Divider() + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/NavigationScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/NavigationScreen.swift new file mode 100644 index 0000000..a9f62bb --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/NavigationScreen.swift @@ -0,0 +1,73 @@ +import AndroidSwiftUICore + +/// Classic and value-based navigation, plus the dismiss environment action. +struct NavigationScreen: View { + + @Environment(\.dismiss) + private var dismiss + + @State + private var showsAlert = false + + var body: some View { + VStack(spacing: 16) { + Button("Show alert") { showsAlert = true } + Text("Value-based navigation") + NavigationLink("Push value 1", value: 1) + NavigationLink(value: 2) { + Text("Push value 2") + } + Divider() + Text("Classic navigation") + NavigationLink("Push destination view", destination: ClassicDestination()) + Divider() + Button("Pop with dismiss") { + dismiss() + } + } + .navigationDestination(for: Int.self) { value in + ValueDestination(value: value) + } + .alert("An alert", isPresented: $showsAlert, message: "This is an alert message.", buttons: [ + AlertButton("Cancel", role: .cancel), + AlertButton("OK"), + ]) + .navigationTitle("Navigation") + } +} + +struct ValueDestination: View { + + let value: Int + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + VStack(spacing: 16) { + Text(verbatim: "Destination for value \(value)") + if value < 3 { + NavigationLink("Push value \(value + 1)", value: value + 1) + } + Button("Pop with dismiss") { + dismiss() + } + } + .navigationTitle("Value \(value)") + } +} + +struct ClassicDestination: View { + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + VStack(spacing: 16) { + Text("Classic destination") + Button("Pop with dismiss") { + dismiss() + } + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/ObservationScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/ObservationScreen.swift new file mode 100644 index 0000000..4c3c01b --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/ObservationScreen.swift @@ -0,0 +1,37 @@ +import AndroidSwiftUICore + +import Observation + +/// Observable objects shared through the environment. +@Observable +final class GalleryModel { + + var counter = 0 +} + +struct ObservationScreen: View { + + @State + private var model = GalleryModel() + + var body: some View { + ObservationCounterView() + .environment(model) + } +} + +struct ObservationCounterView: View { + + @Environment(GalleryModel.self) + private var model + + var body: some View { + VStack(spacing: 16) { + Text("Observable environment object") + Text(verbatim: "Counter: \(model.counter)") + Button("Increment") { + model.counter += 1 + } + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/SheetScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/SheetScreen.swift new file mode 100644 index 0000000..e738d33 --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/SheetScreen.swift @@ -0,0 +1,46 @@ +import AndroidSwiftUICore + +/// Sheet presentation, detents and the dismiss environment action. +struct SheetScreen: View { + + @State + private var showsSheet = false + + @State + private var showsDetentSheet = false + + var body: some View { + VStack(spacing: 16) { + Button("Present sheet") { + showsSheet = true + } + Button("Present medium detent sheet") { + showsDetentSheet = true + } + } + .sheet(isPresented: $showsSheet) { + SheetContent(title: "Full size sheet") + } + .sheet(isPresented: $showsDetentSheet) { + SheetContent(title: "Medium sheet") + .presentationDetents([.medium]) + } + } +} + +struct SheetContent: View { + + let title: String + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + VStack(spacing: 16) { + Text(title) + Button("Dismiss") { + dismiss() + } + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/StacksScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/StacksScreen.swift new file mode 100644 index 0000000..e31a63c --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/StacksScreen.swift @@ -0,0 +1,69 @@ +import AndroidSwiftUICore + +/// Stack containers and cross-axis alignment. +struct StacksScreen: View { + + var body: some View { + ScrollView { + VStack(spacing: 16) { + AlignmentSection(title: "VStack .leading", alignment: .leading) + AlignmentSection(title: "VStack .center", alignment: .center) + AlignmentSection(title: "VStack .trailing", alignment: .trailing) + Divider() + Text("HStack with Spacer") + HStack { + Text("Start") + Spacer() + Text("End") + } + Divider() + Text("LazyVStack") + LazyVStack(alignment: .leading) { + Text("Lazy one") + Text("Lazy two, longer") + } + Divider() + ZStackSection(title: "ZStack .center", alignment: .center) + ZStackSection(title: "ZStack .topLeading", alignment: .topLeading) + ZStackSection(title: "ZStack .bottomTrailing", alignment: .bottomTrailing) + } + } + } +} + +struct ZStackSection: View { + + let title: String + + let alignment: Alignment + + var body: some View { + VStack(spacing: 8) { + Text(title) + ZStack(alignment: alignment) { + Color.blue + .frame(width: 240, height: 120) + Text("On top") + } + Divider() + } + } +} + +struct AlignmentSection: View { + + let title: String + + let alignment: HorizontalAlignment + + var body: some View { + VStack(spacing: 8) { + Text(title) + VStack(alignment: alignment) { + Text("Row one") + Text("Row two, longer") + } + Divider() + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/StateScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/StateScreen.swift new file mode 100644 index 0000000..d742f52 --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/StateScreen.swift @@ -0,0 +1,46 @@ +import AndroidSwiftUICore + +/// Lazy state initialization: the stored class is constructed once per view +/// lifetime, no matter how often the parent re-renders. +struct StateScreen: View { + + @State + private var parentRenders = 0 + + var body: some View { + VStack(spacing: 16) { + Text(verbatim: "Parent re-renders: \(parentRenders)") + Button("Re-render parent") { + parentRenders += 1 + } + Divider() + StateChildView() + } + } +} + +struct StateChildView: View { + + @State + private var model = CountedModel() + + var body: some View { + VStack(spacing: 16) { + Text(verbatim: "Model instance: #\(model.instance)") + Text(verbatim: "Total constructions: \(CountedModel.constructions)") + } + } +} + +/// Counts how many times it has ever been constructed. +final class CountedModel { + + nonisolated(unsafe) static var constructions = 0 + + let instance: Int + + init() { + Self.constructions += 1 + instance = Self.constructions + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/TabScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/TabScreen.swift new file mode 100644 index 0000000..cb4199d --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/TabScreen.swift @@ -0,0 +1,27 @@ +import AndroidSwiftUICore + +/// Tab bar with selection. +struct TabScreen: View { + + @State + private var selection = 0 + + var body: some View { + TabView(selection: $selection) { + VStack(spacing: 16) { + Text("First tab") + Button("Select third tab") { + selection = 2 + } + } + .tabItem { Text("One") } + .tag(0) + Text("Second tab") + .tabItem { Text("Two") } + .tag(1) + Text("Third tab") + .tabItem { Text("Three") } + .tag(2) + } + } +} diff --git a/Sources/SwiftUIDesktopDemo/Gallery/TextScreen.swift b/Sources/SwiftUIDesktopDemo/Gallery/TextScreen.swift new file mode 100644 index 0000000..4285941 --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/Gallery/TextScreen.swift @@ -0,0 +1,21 @@ +import AndroidSwiftUICore + +/// Text initializers and dynamic content. +struct TextScreen: View { + + @State + private var counter = 0 + + var body: some View { + VStack(spacing: 16) { + Text("Plain text") + Text(verbatim: "Verbatim text") + Text("Interpolated counter: \(counter)") + Button("Increment") { + counter += 1 + } + Divider() + Text("Multiline text that should wrap when it becomes longer than a single line on screen") + } + } +} From 63b17058c2a46250102bd8876b00d08422354655 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 02/20] Add shared catalog example row and screen shim --- Demo/App.swiftpm/Sources/CatalogSupport.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/CatalogSupport.swift diff --git a/Demo/App.swiftpm/Sources/CatalogSupport.swift b/Demo/App.swiftpm/Sources/CatalogSupport.swift new file mode 100644 index 0000000..ee59a36 --- /dev/null +++ b/Demo/App.swiftpm/Sources/CatalogSupport.swift @@ -0,0 +1,31 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +/// Type-erased catalog screen (a small AnyView shim usable as a navigation +/// destination while keeping the entry list homogeneous). +struct AnyCatalogScreen: View { + private let content: AnyView + init(_ view: V) { self.content = AnyView(view) } + var body: some View { content } +} + +/// A titled example row shared by the playgrounds. +struct Example: View { + let title: String + let content: Content + init(_ title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + content + Divider() + } + .padding() + } +} From 43bab17d99bb14340595a094f646767123babaf3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 03/20] Add control playgrounds (Text, Button, Toggle, Slider, TextField, Picker, ProgressView) --- .../Sources/ControlPlaygrounds.swift | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/ControlPlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/ControlPlaygrounds.swift b/Demo/App.swiftpm/Sources/ControlPlaygrounds.swift new file mode 100644 index 0000000..c1eaf0e --- /dev/null +++ b/Demo/App.swiftpm/Sources/ControlPlaygrounds.swift @@ -0,0 +1,170 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +struct TextPlayground: View { + @State private var counter = 0 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Plain") { Text("Hello, world") } + Example("Verbatim") { Text(verbatim: "Raw string, no interpolation") } + Example("Interpolated") { Text("Counter is \(counter)") } + Example("Bump the counter") { Button("Increment") { counter += 1 } } + Example("Multiline") { + Text("A longer passage of text that wraps onto multiple lines when it no longer fits within the width of the screen.") + } + Example("Styled") { + Text("Blue on a rounded chip") + .padding() + .background(Color.blue) + .cornerRadius(12) + } + } + } + } +} + +struct ButtonPlayground: View { + @State private var taps = 0 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Taps: \(taps)") { Text("Tap any button below") } + Example("Title initializer") { Button("Tap me") { taps += 1 } } + Example("Label closure") { + Button(action: { taps += 1 }) { Text("Custom label") } + } + Example("Styled label") { + Button(action: { taps += 1 }) { + Text("Padded label") + .padding() + .background(Color.green) + .cornerRadius(8) + } + } + Example("Reset") { Button("Reset to zero") { taps = 0 } } + } + } + } +} + +struct TogglePlayground: View { + @State private var a = false + @State private var b = true + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Off by default") { + VStack(alignment: .leading, spacing: 8) { + Toggle("Enable feature", isOn: $a) + Text(a ? "Feature is on" : "Feature is off") + } + } + Example("On by default") { + VStack(alignment: .leading, spacing: 8) { + Toggle("Notifications", isOn: $b) + Text(b ? "Notifications on" : "Notifications off") + } + } + } + } + } +} + +struct SliderPlayground: View { + @State private var unit = 0.5 + @State private var wide = 25.0 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("0...1") { + VStack(alignment: .leading, spacing: 8) { + Slider(value: $unit, in: 0...1) + Text("Value: \(Int(unit * 100))%") + } + } + Example("0...100") { + VStack(alignment: .leading, spacing: 8) { + Slider(value: $wide, in: 0...100) + Text("Value: \(Int(wide))") + } + } + } + } + } +} + +struct TextFieldPlayground: View { + @State private var name = "" + @State private var city = "" + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Name") { + VStack(alignment: .leading, spacing: 8) { + TextField("Your name", text: $name) + Text(name.isEmpty ? "Nothing typed yet" : "Hello, \(name)") + } + } + Example("City") { + VStack(alignment: .leading, spacing: 8) { + TextField("Your city", text: $city) + Text(city.isEmpty ? "—" : "You are in \(city)") + } + } + } + } + } +} + +struct PickerPlayground: View { + @State private var fruit = "Apple" + @State private var size = 1 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("String selection") { + VStack(alignment: .leading, spacing: 8) { + Picker("Fruit", selection: $fruit) { + Text("Apple").tag("Apple") + Text("Banana").tag("Banana") + Text("Cherry").tag("Cherry") + } + Text("Selected: \(fruit)") + } + } + Example("Int selection") { + VStack(alignment: .leading, spacing: 8) { + Picker("Size", selection: $size) { + Text("Small").tag(0) + Text("Medium").tag(1) + Text("Large").tag(2) + } + Text("Size index: \(size)") + } + } + } + } + } +} + +struct ProgressViewPlayground: View { + @State private var progress = 0.25 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Indeterminate") { ProgressView() } + Example("Determinate") { + VStack(alignment: .leading, spacing: 8) { + ProgressView(value: progress) + Text("\(Int(progress * 100))%") + Button("Advance") { progress = progress >= 1 ? 0 : progress + 0.25 } + } + } + } + } + } +} From 956da259c78bf49b9ff5db7c059f3109124ab181 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 04/20] Add layout playgrounds (Stacks, Spacer, Divider, Color, ScrollView) --- .../Sources/LayoutPlaygrounds.swift | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/LayoutPlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/LayoutPlaygrounds.swift b/Demo/App.swiftpm/Sources/LayoutPlaygrounds.swift new file mode 100644 index 0000000..ba26dd8 --- /dev/null +++ b/Demo/App.swiftpm/Sources/LayoutPlaygrounds.swift @@ -0,0 +1,90 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +struct StackPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("VStack .leading") { + VStack(alignment: .leading) { Text("Row one"); Text("Row two, longer") } + } + Example("VStack .trailing") { + VStack(alignment: .trailing) { Text("Row one"); Text("Row two, longer") } + } + Example("HStack with Spacer") { + HStack { Text("Start"); Spacer(); Text("End") } + } + Example("ZStack .center") { + ZStack { + Color.blue.frame(width: 220, height: 100) + Text("Overlaid") + } + } + Example("ZStack .bottomTrailing") { + ZStack(alignment: .bottomTrailing) { + Color.green.frame(width: 220, height: 100) + Text("Corner") + } + } + } + } + } +} + +struct SpacerDividerPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Spacer pushes apart") { + HStack { Text("Left"); Spacer(); Text("Right") } + } + Example("Divider between rows") { + VStack(alignment: .leading, spacing: 8) { + Text("Above") + Divider() + Text("Below") + } + } + } + } + } +} + +struct ColorPlayground: View { + private let swatches: [(String, Color)] = [ + ("blue", .blue), ("green", .green), ("orange", .orange), + ("purple", .purple), ("pink", .pink), ("red", .red), + ("yellow", .yellow), ("gray", .gray), + ] + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(swatches, id: \.0) { swatch in + Example(swatch.0) { + swatch.1.frame(height: 44).cornerRadius(8) + } + } + } + } + } +} + +struct ScrollViewPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 8) { + Text("Scroll to see all rows") + .padding() + ForEach(1...40, id: \.self) { n in + Text("Scrollable row \(n)") + .padding() + .background(n % 2 == 0 ? Color.blue : Color.orange) + .cornerRadius(6) + } + } + } + } +} From 49bc3c3941b1f9dc43800f0cd62a9bf98f3c872d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 05/20] Add container playgrounds (List, Grid, Modifiers) --- .../Sources/ContainerPlaygrounds.swift | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/ContainerPlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/ContainerPlaygrounds.swift b/Demo/App.swiftpm/Sources/ContainerPlaygrounds.swift new file mode 100644 index 0000000..5c82ef4 --- /dev/null +++ b/Demo/App.swiftpm/Sources/ContainerPlaygrounds.swift @@ -0,0 +1,71 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +struct ListPlayground: View { + @State private var rows = (1...25).map(CatalogRow.init) + var body: some View { + List(rows) { row in + Text(row.title) + } + .refreshable { + try? await Task.sleep(for: .seconds(1)) + rows.insert(CatalogRow(id: rows.count + 1), at: 0) + } + } +} + +struct CatalogRow: Identifiable { + let id: Int + var title: String { "Row \(id)" } +} + +struct GridPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Three fixed columns") { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]) { + ForEach(1...9, id: \.self) { n in + Text("\(n)").padding().background(Color.blue).cornerRadius(6) + } + } + } + Example("Adaptive columns") { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { + ForEach(1...8, id: \.self) { n in + Text("Item \(n)").padding().background(Color.orange).cornerRadius(6) + } + } + } + Example("Two rows, horizontal") { + LazyHGrid(rows: [GridItem(.flexible()), GridItem(.flexible())]) { + ForEach(1...8, id: \.self) { n in + Text("Cell \(n)").padding().background(Color.green).cornerRadius(6) + } + } + .frame(height: 160) + } + } + } + } +} + +struct ModifierPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Padding") { Text("Padded").padding().background(Color.blue) } + Example("Frame 200x60") { Text("Fixed").frame(width: 200, height: 60).background(Color.green) } + Example("Background") { Text("Colored").padding().background(Color.orange) } + Example("Corner radius") { Text("Rounded").padding().background(Color.purple).cornerRadius(16) } + Example("Offset") { Text("Shifted").offset(x: 30, y: 0).background(Color.red) } + Example("Rotation") { Text("Rotated").padding().background(Color.yellow).rotationEffect(.degrees(15)) } + Example("Scale") { Text("Scaled").padding().background(Color.pink).scaleEffect(1.4) } + Example("Opacity") { Text("Faded").padding().background(Color.blue).opacity(0.4) } + } + } + } +} From f1245ac5e08011f35c835d03e1b8bfd46677b01c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 06/20] Add presentation playgrounds (Navigation, TabView, Sheet, Alert) --- .../Sources/PresentationPlaygrounds.swift | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/PresentationPlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/PresentationPlaygrounds.swift b/Demo/App.swiftpm/Sources/PresentationPlaygrounds.swift new file mode 100644 index 0000000..5fe8b88 --- /dev/null +++ b/Demo/App.swiftpm/Sources/PresentationPlaygrounds.swift @@ -0,0 +1,108 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +struct NavigationPlayground: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("Value-based navigation").padding() + NavigationLink("Push value 1", value: 1) + NavigationLink("Push value 2", value: 2) + Divider() + Text("Classic navigation").padding() + NavigationLink("Push a destination view", destination: NavDetail(label: "Pushed directly")) + } + } + .navigationDestination(for: Int.self) { value in + NavDetail(label: "Value \(value)", next: value + 1) + } + } +} + +struct NavDetail: View { + let label: String + var next: Int? = nil + @Environment(\.dismiss) private var dismiss + var body: some View { + VStack(spacing: 16) { + Text(label) + if let next, next <= 4 { + NavigationLink("Push value \(next)", value: next) + } + Button("Pop with dismiss") { dismiss() } + } + .padding() + .navigationTitle(label) + } +} + +struct TabViewPlayground: View { + @State private var selection = 0 + var body: some View { + TabView(selection: $selection) { + VStack(spacing: 16) { + Text("First tab content") + Button("Jump to third tab") { selection = 2 } + } + .tabItem { Text("One") }.tag(0) + Text("Second tab content").tabItem { Text("Two") }.tag(1) + Text("Third tab content").tabItem { Text("Three") }.tag(2) + } + } +} + +struct SheetPlayground: View { + @State private var full = false + @State private var medium = false + var body: some View { + ScrollView { + VStack(spacing: 16) { + Button("Present full-size sheet") { full = true } + Button("Present medium detent sheet") { medium = true } + } + .padding() + } + .sheet(isPresented: $full) { + SheetBody(title: "Full-size sheet") + } + .sheet(isPresented: $medium) { + SheetBody(title: "Medium sheet").presentationDetents([.medium]) + } + } +} + +struct SheetBody: View { + let title: String + @Environment(\.dismiss) private var dismiss + var body: some View { + VStack(spacing: 16) { + Text(title) + Button("Dismiss") { dismiss() } + } + .padding() + } +} + +struct AlertPlayground: View { + @State private var simple = false + @State private var confirm = false + @State private var result = "No choice yet" + var body: some View { + ScrollView { + VStack(spacing: 16) { + Button("Show simple alert") { simple = true } + Button("Show confirmation") { confirm = true } + Text(result) + } + .padding() + } + .alert("A simple alert", isPresented: $simple, message: "This is the message body.") + .alert("Delete item?", isPresented: $confirm, message: "This cannot be undone.", buttons: [ + AlertButton("Cancel", role: .cancel) { result = "Cancelled" }, + AlertButton("Delete", role: .destructive) { result = "Deleted" }, + ]) + } +} From 87277a8c5426e16af3ec6ddde4c40869bc344110 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:35 -0400 Subject: [PATCH 07/20] Add state playgrounds (State, Environment, Observable) --- .../Sources/StatePlaygrounds.swift | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/StatePlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/StatePlaygrounds.swift b/Demo/App.swiftpm/Sources/StatePlaygrounds.swift new file mode 100644 index 0000000..af9aac2 --- /dev/null +++ b/Demo/App.swiftpm/Sources/StatePlaygrounds.swift @@ -0,0 +1,82 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +import Observation + +struct StatePlayground: View { + @State private var parentRenders = 0 + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Parent re-renders: \(parentRenders)") { + Button("Re-render parent") { parentRenders += 1 } + } + Example("Child keeps its own state") { + StateChild() + } + } + } + } +} + +struct StateChild: View { + @State private var localCount = 0 + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Local count: \(localCount)") + Button("Bump child") { localCount += 1 } + Text("Survives parent re-renders") + } + } +} + +@Observable +final class CounterModel { + var value = 0 +} + +struct EnvironmentPlayground: View { + @State private var model = CounterModel() + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Injected object") { + EnvReader().environment(model) + } + Example("Read here too") { + EnvReader().environment(model) + } + } + } + } +} + +struct EnvReader: View { + @Environment(CounterModel.self) private var model + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Shared value: \(model.value)") + Button("Increment shared") { model.value += 1 } + } + } +} + +struct ObservablePlayground: View { + @State private var model = CounterModel() + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Observation drives updates") { + VStack(alignment: .leading, spacing: 8) { + Text("Observable value: \(model.value)") + Button("Increment") { model.value += 1 } + Text("Mutating the model re-evaluates automatically") + } + } + } + } + } +} From ec6375e5c9aadc2e69e66ac8a84aa9de716b57ee Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:47 -0400 Subject: [PATCH 08/20] Replace demo root with component catalog --- Demo/App.swiftpm/Sources/Catalog.swift | 49 ++++++++++++++++++++++ Demo/App.swiftpm/Sources/ContentView.swift | 33 --------------- 2 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 Demo/App.swiftpm/Sources/Catalog.swift delete mode 100644 Demo/App.swiftpm/Sources/ContentView.swift diff --git a/Demo/App.swiftpm/Sources/Catalog.swift b/Demo/App.swiftpm/Sources/Catalog.swift new file mode 100644 index 0000000..b758bff --- /dev/null +++ b/Demo/App.swiftpm/Sources/Catalog.swift @@ -0,0 +1,49 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif + +/// A component catalog: one playground per supported SwiftUI feature, navigated +/// with a `NavigationStack` and a lazy `List`. +struct ContentView: View { + + var body: some View { + NavigationStack { + List(CatalogEntry.all) { entry in + NavigationLink(entry.title, destination: entry.screen) + } + .navigationTitle("Catalog") + } + } +} + +struct CatalogEntry: Identifiable { + let id: String + let title: String + let screen: AnyCatalogScreen + + static let all: [CatalogEntry] = [ + CatalogEntry(id: "text", title: "Text", screen: AnyCatalogScreen(TextPlayground())), + CatalogEntry(id: "button", title: "Button", screen: AnyCatalogScreen(ButtonPlayground())), + CatalogEntry(id: "toggle", title: "Toggle", screen: AnyCatalogScreen(TogglePlayground())), + CatalogEntry(id: "slider", title: "Slider", screen: AnyCatalogScreen(SliderPlayground())), + CatalogEntry(id: "textfield", title: "TextField", screen: AnyCatalogScreen(TextFieldPlayground())), + CatalogEntry(id: "picker", title: "Picker", screen: AnyCatalogScreen(PickerPlayground())), + CatalogEntry(id: "progress", title: "ProgressView", screen: AnyCatalogScreen(ProgressViewPlayground())), + CatalogEntry(id: "stack", title: "Stacks", screen: AnyCatalogScreen(StackPlayground())), + CatalogEntry(id: "spacer", title: "Spacer & Divider", screen: AnyCatalogScreen(SpacerDividerPlayground())), + CatalogEntry(id: "color", title: "Color", screen: AnyCatalogScreen(ColorPlayground())), + 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: "modifier", title: "Modifiers", screen: AnyCatalogScreen(ModifierPlayground())), + CatalogEntry(id: "navigation", title: "Navigation", screen: AnyCatalogScreen(NavigationPlayground())), + CatalogEntry(id: "tab", title: "TabView", screen: AnyCatalogScreen(TabViewPlayground())), + CatalogEntry(id: "sheet", title: "Sheet", screen: AnyCatalogScreen(SheetPlayground())), + CatalogEntry(id: "alert", title: "Alert", screen: AnyCatalogScreen(AlertPlayground())), + CatalogEntry(id: "state", title: "State", screen: AnyCatalogScreen(StatePlayground())), + CatalogEntry(id: "environment", title: "Environment", screen: AnyCatalogScreen(EnvironmentPlayground())), + CatalogEntry(id: "observable", title: "Observable", screen: AnyCatalogScreen(ObservablePlayground())), + ] +} diff --git a/Demo/App.swiftpm/Sources/ContentView.swift b/Demo/App.swiftpm/Sources/ContentView.swift deleted file mode 100644 index c3f81bf..0000000 --- a/Demo/App.swiftpm/Sources/ContentView.swift +++ /dev/null @@ -1,33 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Gallery of playground screens, navigated with a `NavigationStack`. Each row -/// pushes a feature screen; `List`-based navigation returns with lazy -/// containers in the next step. -struct ContentView: View { - - var body: some View { - NavigationStack { - ScrollView { - VStack(spacing: 0) { - NavigationLink("Text", destination: TextScreen()) - NavigationLink("Buttons", destination: ButtonScreen()) - NavigationLink("Stacks", destination: StacksScreen()) - NavigationLink("List", destination: ListScreen()) - NavigationLink("Grid", destination: GridScreen()) - NavigationLink("State", destination: StateScreen()) - NavigationLink("Controls", destination: ControlsScreen()) - NavigationLink("Modifiers", destination: ModifierScreen()) - NavigationLink("Observation", destination: ObservationScreen()) - NavigationLink("Navigation", destination: NavigationScreen()) - NavigationLink("Sheets", destination: SheetScreen()) - NavigationLink("Tabs", destination: TabScreen()) - } - } - .navigationTitle("Gallery") - } - } -} From 53ca1769c80ca49b098148d7328173cd69ec7f41 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:59 -0400 Subject: [PATCH 09/20] Remove old ButtonScreen gallery screen --- Demo/App.swiftpm/Sources/ButtonScreen.swift | 30 --------------------- 1 file changed, 30 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/ButtonScreen.swift diff --git a/Demo/App.swiftpm/Sources/ButtonScreen.swift b/Demo/App.swiftpm/Sources/ButtonScreen.swift deleted file mode 100644 index dfddee3..0000000 --- a/Demo/App.swiftpm/Sources/ButtonScreen.swift +++ /dev/null @@ -1,30 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Button initializers and actions. -struct ButtonScreen: View { - - @State - private var tapCount = 0 - - var body: some View { - VStack(spacing: 16) { - Text(verbatim: "Taps: \(tapCount)") - Button("Title initializer") { - tapCount += 1 - } - Button(action: { tapCount += 1 }) { - Text("Label closure initializer") - } - Button(action: { tapCount += 1 }) { - Image("globe") - } - Button("Reset") { - tapCount = 0 - } - } - } -} From 02348ad6af7d0a09a1cb1fc1d9b9d27fd7941de0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:59 -0400 Subject: [PATCH 10/20] Remove old ControlsScreen gallery screen --- Demo/App.swiftpm/Sources/ControlsScreen.swift | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/ControlsScreen.swift diff --git a/Demo/App.swiftpm/Sources/ControlsScreen.swift b/Demo/App.swiftpm/Sources/ControlsScreen.swift deleted file mode 100644 index 82ab822..0000000 --- a/Demo/App.swiftpm/Sources/ControlsScreen.swift +++ /dev/null @@ -1,60 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Form controls and their state bindings. -struct ControlsScreen: View { - - @State - private var isOn = false - - @State - private var progress = 0.25 - - @State - private var sliderValue = 0.5 - - @State - private var name = "" - - @State - private var fruit = "Apple" - - var body: some View { - ScrollView { - VStack(spacing: 24) { - Text("Toggle") - Toggle("Enabled", isOn: $isOn) - Text(isOn ? "Toggle is on" : "Toggle is off") - Divider() - Text("Indeterminate progress") - ProgressView() - Divider() - Text("Determinate progress") - ProgressView(value: progress) - Text("\(Int(progress * 100))%") - Button("Advance") { - progress = progress >= 1 ? 0 : progress + 0.25 - } - Divider() - Text("Slider") - Slider(value: $sliderValue, in: 0...1) - Text("Value: \(Int(sliderValue * 100))%") - Divider() - Text("Text field") - TextField("Name", text: $name) - Text(name.isEmpty ? "Nothing typed yet" : "Hello, \(name)") - Divider() - Text("Picker") - Picker("Fruit", selection: $fruit) { - Text("Apple").tag("Apple") - Text("Banana").tag("Banana") - Text("Cherry").tag("Cherry") - } - Text("Selected: \(fruit)") - } - } - } -} From 09bae3fd79a84bb1360b91eb997807d1804ec5ca Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:41:59 -0400 Subject: [PATCH 11/20] Remove old GridScreen gallery screen --- Demo/App.swiftpm/Sources/GridScreen.swift | 41 ----------------------- 1 file changed, 41 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/GridScreen.swift diff --git a/Demo/App.swiftpm/Sources/GridScreen.swift b/Demo/App.swiftpm/Sources/GridScreen.swift deleted file mode 100644 index 9dd00cc..0000000 --- a/Demo/App.swiftpm/Sources/GridScreen.swift +++ /dev/null @@ -1,41 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Lazy grids. -struct GridScreen: View { - - var body: some View { - VStack(spacing: 16) { - Text("Three fixed columns") - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]) { - ForEach(1...9, id: \.self) { number in - Text("\(number)") - .padding() - .background(Color.blue) - } - } - Divider() - Text("Adaptive columns") - LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) { - ForEach(1...8, id: \.self) { number in - Text("Item \(number)") - .padding() - .background(Color.orange) - } - } - Divider() - Text("Two rows, horizontal") - LazyHGrid(rows: [GridItem(.flexible()), GridItem(.flexible())]) { - ForEach(1...8, id: \.self) { number in - Text("Cell \(number)") - .padding() - .background(Color.green) - } - } - .frame(height: 160) - } - } -} From 456d9a20863a62b537bf66378bb7a4710d890dec Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 12/20] Remove old ListScreen gallery screen --- Demo/App.swiftpm/Sources/ListScreen.swift | 36 ----------------------- 1 file changed, 36 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/ListScreen.swift diff --git a/Demo/App.swiftpm/Sources/ListScreen.swift b/Demo/App.swiftpm/Sources/ListScreen.swift deleted file mode 100644 index b46c9d9..0000000 --- a/Demo/App.swiftpm/Sources/ListScreen.swift +++ /dev/null @@ -1,36 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// A lazy list with pull to refresh. Rows are evaluated on demand, so the -/// large-list variant scrolls without materializing every row up front. -struct ListScreen: View { - - @State - private var items = (1...30).map(ListItem.init) - - var body: some View { - List(items) { item in - Text(item.title) - } - .refreshable { - await addItem() - } - } - - private func addItem() async { - try? await Task.sleep(for: .seconds(1)) - items.insert(ListItem(id: items.count + 1), at: 0) - } -} - -struct ListItem: Identifiable { - - let id: Int - - var title: String { - "Row \(id)" - } -} From 43d2f5b6b5b2711101d2f4ae12bf56aa6a0efbed Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 13/20] Remove old ModifierScreen gallery screen --- Demo/App.swiftpm/Sources/ModifierScreen.swift | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/ModifierScreen.swift diff --git a/Demo/App.swiftpm/Sources/ModifierScreen.swift b/Demo/App.swiftpm/Sources/ModifierScreen.swift deleted file mode 100644 index 57bb779..0000000 --- a/Demo/App.swiftpm/Sources/ModifierScreen.swift +++ /dev/null @@ -1,72 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Layout and effect modifiers. -struct ModifierScreen: View { - - var body: some View { - ScrollView { - VStack(spacing: 24) { - ModifierSection(title: "Padding") { - Text("Padded") - .padding() - .background(Color.blue) - } - ModifierSection(title: "Frame") { - Text("Fixed 200x60") - .frame(width: 200, height: 60) - .background(Color.green) - } - ModifierSection(title: "Background") { - Text("Colored background") - .padding() - .background(Color.orange) - } - ModifierSection(title: "Clip + corner radius") { - Text("Rounded") - .padding() - .background(Color.purple) - .cornerRadius(16) - } - ModifierSection(title: "Offset") { - Text("Shifted") - .offset(x: 30, y: 0) - .background(Color.red) - } - ModifierSection(title: "Rotation") { - Text("Rotated") - .background(Color.yellow) - .rotationEffect(.degrees(15)) - } - ModifierSection(title: "Scale") { - Text("Scaled") - .background(Color.pink) - .scaleEffect(1.5) - } - } - } - } -} - -struct ModifierSection: View { - - let title: String - - let content: Content - - init(title: String, @ViewBuilder content: () -> Content) { - self.title = title - self.content = content() - } - - var body: some View { - VStack(spacing: 8) { - Text(title) - content - Divider() - } - } -} From 6c672bf22f6f6859ca695f32d442f71a292729eb Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 14/20] Remove old NavigationScreen gallery screen --- .../Sources/NavigationScreen.swift | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/NavigationScreen.swift diff --git a/Demo/App.swiftpm/Sources/NavigationScreen.swift b/Demo/App.swiftpm/Sources/NavigationScreen.swift deleted file mode 100644 index 251f973..0000000 --- a/Demo/App.swiftpm/Sources/NavigationScreen.swift +++ /dev/null @@ -1,77 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Classic and value-based navigation, plus the dismiss environment action. -struct NavigationScreen: View { - - @Environment(\.dismiss) - private var dismiss - - @State - private var showsAlert = false - - var body: some View { - VStack(spacing: 16) { - Button("Show alert") { showsAlert = true } - Text("Value-based navigation") - NavigationLink("Push value 1", value: 1) - NavigationLink(value: 2) { - Text("Push value 2") - } - Divider() - Text("Classic navigation") - NavigationLink("Push destination view", destination: ClassicDestination()) - Divider() - Button("Pop with dismiss") { - dismiss() - } - } - .navigationDestination(for: Int.self) { value in - ValueDestination(value: value) - } - .alert("An alert", isPresented: $showsAlert, message: "This is an alert message.", buttons: [ - AlertButton("Cancel", role: .cancel), - AlertButton("OK"), - ]) - .navigationTitle("Navigation") - } -} - -struct ValueDestination: View { - - let value: Int - - @Environment(\.dismiss) - private var dismiss - - var body: some View { - VStack(spacing: 16) { - Text(verbatim: "Destination for value \(value)") - if value < 3 { - NavigationLink("Push value \(value + 1)", value: value + 1) - } - Button("Pop with dismiss") { - dismiss() - } - } - .navigationTitle("Value \(value)") - } -} - -struct ClassicDestination: View { - - @Environment(\.dismiss) - private var dismiss - - var body: some View { - VStack(spacing: 16) { - Text("Classic destination") - Button("Pop with dismiss") { - dismiss() - } - } - } -} From e10222483003957de9e3c03a02cea22981f9b042 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 15/20] Remove old ObservationScreen gallery screen --- .../Sources/ObservationScreen.swift | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/ObservationScreen.swift diff --git a/Demo/App.swiftpm/Sources/ObservationScreen.swift b/Demo/App.swiftpm/Sources/ObservationScreen.swift deleted file mode 100644 index 3678f1f..0000000 --- a/Demo/App.swiftpm/Sources/ObservationScreen.swift +++ /dev/null @@ -1,41 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -import Observation - -/// Observable objects shared through the environment. -@Observable -final class GalleryModel { - - var counter = 0 -} - -struct ObservationScreen: View { - - @State - private var model = GalleryModel() - - var body: some View { - ObservationCounterView() - .environment(model) - } -} - -struct ObservationCounterView: View { - - @Environment(GalleryModel.self) - private var model - - var body: some View { - VStack(spacing: 16) { - Text("Observable environment object") - Text(verbatim: "Counter: \(model.counter)") - Button("Increment") { - model.counter += 1 - } - } - } -} From c35e2a243650e3cb49be753d0717c13a7bd166ec Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 16/20] Remove old SheetScreen gallery screen --- Demo/App.swiftpm/Sources/SheetScreen.swift | 50 ---------------------- 1 file changed, 50 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/SheetScreen.swift diff --git a/Demo/App.swiftpm/Sources/SheetScreen.swift b/Demo/App.swiftpm/Sources/SheetScreen.swift deleted file mode 100644 index 2db7c54..0000000 --- a/Demo/App.swiftpm/Sources/SheetScreen.swift +++ /dev/null @@ -1,50 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Sheet presentation, detents and the dismiss environment action. -struct SheetScreen: View { - - @State - private var showsSheet = false - - @State - private var showsDetentSheet = false - - var body: some View { - VStack(spacing: 16) { - Button("Present sheet") { - showsSheet = true - } - Button("Present medium detent sheet") { - showsDetentSheet = true - } - } - .sheet(isPresented: $showsSheet) { - SheetContent(title: "Full size sheet") - } - .sheet(isPresented: $showsDetentSheet) { - SheetContent(title: "Medium sheet") - .presentationDetents([.medium]) - } - } -} - -struct SheetContent: View { - - let title: String - - @Environment(\.dismiss) - private var dismiss - - var body: some View { - VStack(spacing: 16) { - Text(title) - Button("Dismiss") { - dismiss() - } - } - } -} From 55252ad744bc83d827c4c9a9c9ea7e7f1806a7c9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 17/20] Remove old StacksScreen gallery screen --- Demo/App.swiftpm/Sources/StacksScreen.swift | 73 --------------------- 1 file changed, 73 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/StacksScreen.swift diff --git a/Demo/App.swiftpm/Sources/StacksScreen.swift b/Demo/App.swiftpm/Sources/StacksScreen.swift deleted file mode 100644 index 02ec94c..0000000 --- a/Demo/App.swiftpm/Sources/StacksScreen.swift +++ /dev/null @@ -1,73 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Stack containers and cross-axis alignment. -struct StacksScreen: View { - - var body: some View { - ScrollView { - VStack(spacing: 16) { - AlignmentSection(title: "VStack .leading", alignment: .leading) - AlignmentSection(title: "VStack .center", alignment: .center) - AlignmentSection(title: "VStack .trailing", alignment: .trailing) - Divider() - Text("HStack with Spacer") - HStack { - Text("Start") - Spacer() - Text("End") - } - Divider() - Text("LazyVStack") - LazyVStack(alignment: .leading) { - Text("Lazy one") - Text("Lazy two, longer") - } - Divider() - ZStackSection(title: "ZStack .center", alignment: .center) - ZStackSection(title: "ZStack .topLeading", alignment: .topLeading) - ZStackSection(title: "ZStack .bottomTrailing", alignment: .bottomTrailing) - } - } - } -} - -struct ZStackSection: View { - - let title: String - - let alignment: Alignment - - var body: some View { - VStack(spacing: 8) { - Text(title) - ZStack(alignment: alignment) { - Color.blue - .frame(width: 240, height: 120) - Text("On top") - } - Divider() - } - } -} - -struct AlignmentSection: View { - - let title: String - - let alignment: HorizontalAlignment - - var body: some View { - VStack(spacing: 8) { - Text(title) - VStack(alignment: alignment) { - Text("Row one") - Text("Row two, longer") - } - Divider() - } - } -} From ecbb95eeba7467403a1174f1130ac36a47f9a0c2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 18/20] Remove old StateScreen gallery screen --- Demo/App.swiftpm/Sources/StateScreen.swift | 50 ---------------------- 1 file changed, 50 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/StateScreen.swift diff --git a/Demo/App.swiftpm/Sources/StateScreen.swift b/Demo/App.swiftpm/Sources/StateScreen.swift deleted file mode 100644 index 35f1eb5..0000000 --- a/Demo/App.swiftpm/Sources/StateScreen.swift +++ /dev/null @@ -1,50 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Lazy state initialization: the stored class is constructed once per view -/// lifetime, no matter how often the parent re-renders. -struct StateScreen: View { - - @State - private var parentRenders = 0 - - var body: some View { - VStack(spacing: 16) { - Text(verbatim: "Parent re-renders: \(parentRenders)") - Button("Re-render parent") { - parentRenders += 1 - } - Divider() - StateChildView() - } - } -} - -struct StateChildView: View { - - @State - private var model = CountedModel() - - var body: some View { - VStack(spacing: 16) { - Text(verbatim: "Model instance: #\(model.instance)") - Text(verbatim: "Total constructions: \(CountedModel.constructions)") - } - } -} - -/// Counts how many times it has ever been constructed. -final class CountedModel { - - nonisolated(unsafe) static var constructions = 0 - - let instance: Int - - init() { - Self.constructions += 1 - instance = Self.constructions - } -} From ea48058821f86882619a8201a45feb8353feb669 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 19/20] Remove old TabScreen gallery screen --- Demo/App.swiftpm/Sources/TabScreen.swift | 31 ------------------------ 1 file changed, 31 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/TabScreen.swift diff --git a/Demo/App.swiftpm/Sources/TabScreen.swift b/Demo/App.swiftpm/Sources/TabScreen.swift deleted file mode 100644 index 80ca2e3..0000000 --- a/Demo/App.swiftpm/Sources/TabScreen.swift +++ /dev/null @@ -1,31 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Tab bar with selection. -struct TabScreen: View { - - @State - private var selection = 0 - - var body: some View { - TabView(selection: $selection) { - VStack(spacing: 16) { - Text("First tab") - Button("Select third tab") { - selection = 2 - } - } - .tabItem { Text("One") } - .tag(0) - Text("Second tab") - .tabItem { Text("Two") } - .tag(1) - Text("Third tab") - .tabItem { Text("Three") } - .tag(2) - } - } -} From 2abf648bf7ef3c9c49cf3da8050c4abfa6498f50 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 22 Jul 2026 23:42:00 -0400 Subject: [PATCH 20/20] Remove old TextScreen gallery screen --- Demo/App.swiftpm/Sources/TextScreen.swift | 25 ----------------------- 1 file changed, 25 deletions(-) delete mode 100644 Demo/App.swiftpm/Sources/TextScreen.swift diff --git a/Demo/App.swiftpm/Sources/TextScreen.swift b/Demo/App.swiftpm/Sources/TextScreen.swift deleted file mode 100644 index 99d016d..0000000 --- a/Demo/App.swiftpm/Sources/TextScreen.swift +++ /dev/null @@ -1,25 +0,0 @@ -#if canImport(AndroidSwiftUI) -import AndroidSwiftUI -#else -import SwiftUI -#endif - -/// Text initializers and dynamic content. -struct TextScreen: View { - - @State - private var counter = 0 - - var body: some View { - VStack(spacing: 16) { - Text("Plain text") - Text(verbatim: "Verbatim text") - Text("Interpolated counter: \(counter)") - Button("Increment") { - counter += 1 - } - Divider() - Text("Multiline text that should wrap when it becomes longer than a single line on screen") - } - } -}