From ae847267c7829bc61b5eb3e3a61381599c182d96 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 22:17:07 -0400 Subject: [PATCH 1/2] Honor sheet detents so a sheet fills the height it asked for --- .../kotlin/com/pureswift/swiftui/Render.kt | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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 8b9f713..3b62890 100644 --- a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt @@ -90,6 +90,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.NavigationBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.NavigationBarItem @@ -1719,8 +1720,25 @@ private fun RenderSheetsAndAlerts(screen: ViewNode?) { when (child.type) { "Sheet" -> { val onDismiss = child.long("onDismiss") - ModalBottomSheet(onDismissRequest = { onDismiss?.let { SwiftBridge.sink.invokeVoid(it) } }) { - child.children.firstOrNull()?.let { Render(it) } + // Detents were emitted but never read, so every sheet wrapped its + // content and a "full-size" sheet came up as a short strip. + val detents = child.stringArray("detents") + val medium = detents.contains("medium") + val large = detents.isEmpty() || detents.contains("large") + ModalBottomSheet( + onDismissRequest = { onDismiss?.let { SwiftBridge.sink.invokeVoid(it) } }, + // with no medium detent there is no partial stop to rest at + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = !medium), + ) { + // A Material sheet is only as tall as its content, so one that + // should own the screen has to be told to fill it. + val height = when { + medium && !large -> Modifier.fillMaxHeight(0.5f) + else -> Modifier.fillMaxHeight() + } + Box(modifier = height) { + child.children.firstOrNull()?.let { Render(it) } + } } } "Alert" -> RenderAlert(child) From 9c7b42ff1cbee2efe96fca459572dc9e7f6be3dc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 22:17:07 -0400 Subject: [PATCH 2/2] Test sheet detent emission --- .../NavigationTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/NavigationTests.swift b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/NavigationTests.swift index d9dc464..7acfdd9 100644 --- a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/NavigationTests.swift +++ b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/NavigationTests.swift @@ -226,6 +226,34 @@ struct NavigationTests { #expect(host.callbacks.callback(for: tap) != nil) } + @Test("A sheet carries its detents, and none by default") + func sheetDetents() { + struct Screen: View { + @State var shown = true + var body: some View { + Button("Show") { shown = true } + .sheet(isPresented: $shown) { + Text("body").presentationDetents([.medium]) + } + } + } + let node = ViewHost(Screen()).evaluate() + let sheet = node.children.first { $0.type == "Sheet" } + #expect(sheet?.props["detents"] == .array([.string("medium")])) + + // no detents declared: the interpreter reads that as a full-height sheet + struct Plain: View { + @State var shown = true + var body: some View { + Button("Show") { shown = true } + .sheet(isPresented: $shown) { Text("body") } + } + } + let plain = ViewHost(Plain()).evaluate() + let plainSheet = plain.children.first { $0.type == "Sheet" } + #expect(plainSheet?.props["detents"] == .array([])) + } + @Test("TabView emits tabs with their item labels and selection") func tabs() { struct Screen: View {