-
Notifications
You must be signed in to change notification settings - Fork 32
Open
Labels
bugSomething isn't workingSomething isn't workingcomposeLimitation of Jetpack Compose or issue with SwiftUI translationLimitation of Jetpack Compose or issue with SwiftUI translationlayoutSwiftUI/Jetpack Compose layout issuesSwiftUI/Jetpack Compose layout issues
Description
import SwiftUI
#if SKIP
import androidx.compose.material3.TopAppBarDefaults
#endif
// SKIP INSERT: @OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
struct ContentView: View {
var body: some View {
NavigationStack {
Greeting()
.navigationTitle("test1")
.navigationBarTitleDisplayMode(.inline)
}
#if SKIP
.material3TopAppBar { options in
return options.copy(
scrollBehavior: TopAppBarDefaults.exitUntilCollapsedScrollBehavior(),
colors: TopAppBarDefaults.topAppBarColors(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
scrolledContainerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
}
#endif
}
}
struct Greeting: View {
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
ForEach(0..<12) { i in
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
.padding()
}
}
.padding(geometry.safeAreaInsets)
}
.ignoresSafeArea()
}
}
}In this example code, we're setting exitUntilCollapsedScrollBehavior on the top app bar, which will make the top app bar scroll away as you scroll the screen. To prevent layout shift / judder as we scroll, we're using .ignoresSafeArea(), which allows the content to render underneath the top app bar.
But, to my surprise, when I add modifier: modifier.onGloballyPositionedInWindow in ContentLayouts.swift, like this:
Diff of logging changes
diff --git c/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift w/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
index ec0c980..f12b230 100644
--- c/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
+++ w/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
@@ -1,6 +1,7 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP
+import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -120,16 +121,23 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? {
/// - Parameter checkEdges: Which edges to check to see if we're against a safe area. Any matching edges will be
/// passed to the given closure.
@Composable func IgnoresSafeAreaLayout(expandInto: Edge.Set, checkEdges: Edge.Set = [], modifier: Modifier = Modifier, target: @Composable (IntRect, Edge.Set) -> Void) {
+ // Generate a unique ID for this layout instance that persists across recompositions
+ let layoutId = remember {
+ staticLayoutIdCounter += 1
+ return staticLayoutIdCounter
+ }
+
guard let safeArea = EnvironmentValues.shared._safeArea else {
target(IntRect.Zero, [])
return
}
-
+
// Note: We only allow edges we're interested in to affect our internal state and output. This is critical
// for reducing recompositions, especially during e.g. navigation animations. We also match our internal
// state to our output to ensure we aren't re-calling the target block when output hasn't changed
let edgesState = remember { mutableStateOf(checkEdges) }
let edges = edgesState.value
+ Log.d("IgnoresSafeAreaLayout", "[ID:\(layoutId)] edges - \(edges)")
var expansionTop = 0
if expandInto.contains(Edge.Set.top) && edges.contains(Edge.Set.top) {
expansionTop = Int(safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top)
@@ -170,6 +178,7 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? {
return ComposeResult.ok
} in: {
Layout(modifier: modifier.onGloballyPositionedInWindow {
+ Log.d("IgnoresSafeAreaLayout", "[ID:\(layoutId)] onGloballyPositionedInWindow - bounds: (top=\($0.top), left=\($0.left), bottom=\($0.bottom), right=\($0.right), width=\($0.width), height=\($0.height))")
let edges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: expandInto.union(checkEdges))
edgesState.value = edges
}, content: {
@@ -276,6 +285,9 @@ private func adjacentSafeAreaEdges(bounds: Rect, safeArea: SafeArea, isRTL: Bool
}
}
+// Static counter for generating unique layout IDs
+private var staticLayoutIdCounter = 0
+
private func constraint(_ value: Int, subtracting: Int) -> Int {
guard value != Int.MAX_VALUE else {
return valueI find that the resolved height of the layout fluctuates. I expected its resolved hight to always be 2400, the height of my emulator's screen.
Example log
2026-01-11 20:12:08.624 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=1.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2399.0)
2026-01-11 20:12:08.646 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=15.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2385.0)
2026-01-11 20:12:08.666 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=20.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2380.0)
2026-01-11 20:12:08.680 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=22.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2378.0)
2026-01-11 20:12:08.701 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=19.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2381.0)
2026-01-11 20:12:08.720 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=17.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2383.0)
2026-01-11 20:12:08.736 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=16.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2384.0)
2026-01-11 20:12:08.750 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=11.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2389.0)
2026-01-11 20:12:08.764 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=10.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2390.0)
2026-01-11 20:12:08.781 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=5.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2395.0)
2026-01-11 20:12:08.796 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=8.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2392.0)
2026-01-11 20:12:08.812 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=4.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2396.0)
2026-01-11 20:12:08.828 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=7.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2393.0)
2026-01-11 20:12:08.845 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=6.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2394.0)
2026-01-11 20:12:08.861 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=7.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2393.0)
2026-01-11 20:12:08.876 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=0.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2400.0)
2026-01-11 20:12:08.891 7596-7596 IgnoresSafeAreaLayout org.appfair.app.ShowcaseLite D [ID:3] onGloballyPositionedInWindow - bounds: (top=0.0, left=0.0, bottom=2400.0, right=1080.0, width=1080.0, height=2400.0)
The result is a slight layout shift / judder as you scroll the top app bar into and out of the viewport.
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingcomposeLimitation of Jetpack Compose or issue with SwiftUI translationLimitation of Jetpack Compose or issue with SwiftUI translationlayoutSwiftUI/Jetpack Compose layout issuesSwiftUI/Jetpack Compose layout issues