diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeExtensions.swift b/Sources/SkipUI/SkipUI/Compose/ComposeExtensions.swift index b0c9a156..662193d5 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeExtensions.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeExtensions.swift @@ -11,6 +11,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -60,6 +64,26 @@ extension Modifier { let end = layoutDirection == androidx.compose.ui.unit.LayoutDirection.Rtl ? left : right return self.padding(top: top, start: start, bottom: bottom, end: end) } + + /// Invoke the given closure with the modified view's root bounds. + @Composable func onGloballyPositionedInRoot(perform: (Rect) -> Void) -> Modifier { + return self.onGloballyPositioned { + let bounds = $0.boundsInRoot() + if bounds != Rect.Zero { + perform(bounds) + } + } + } + + /// Invoke the given closure with the modified view's window bounds. + @Composable func onGloballyPositionedInWindow(perform: (Rect) -> Void) -> Modifier { + return self.onGloballyPositioned { + let bounds = $0.boundsInWindow() + if bounds != Rect.Zero { + perform(bounds) + } + } + } } extension PaddingValues { diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift b/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift index b5bd00ce..a7b939ee 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift @@ -11,12 +11,18 @@ import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -116,95 +122,144 @@ import androidx.compose.ui.unit.dp } /// Layout the given view to ignore the given safe areas. -@Composable func IgnoresSafeAreaLayout(view: View, edges: Edge.Set, context: ComposeContext) { - IgnoresSafeAreaLayout(edges: edges, context: context) { view.Compose($0) } +@Composable func IgnoresSafeAreaLayout(view: View, context: ComposeContext, expandInto: Edge.Set) { + ComposeContainer(modifier: context.modifier) { modifier in + IgnoresSafeAreaLayout(expandInto: expandInto, modifier: modifier) { _, _ in + view.Compose(context.content()) + } + } } -@Composable func IgnoresSafeAreaLayout(edges: Edge.Set, context: ComposeContext, target: @Composable (ComposeContext) -> Void) { +/// Layout the given content ignoring the given safe areas. +/// +/// - Parameter expandInto: Which safe area edges to expand into, if adjacent. Any expansion will be passed to +/// the given closure as a pixel rect. +/// - 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) { guard let safeArea = EnvironmentValues.shared._safeArea else { - target(context) + target(IntRect.Zero, []) return } - var (safeLeft, safeTop, safeRight, safeBottom) = safeArea.safeBoundsPx - var topPx = 0 - if edges.contains(.top) { - topPx = Int(safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top) - safeTop = safeArea.presentationBoundsPx.top + // 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 + var expansionTop = 0 + if expandInto.contains(Edge.Set.top) && edges.contains(Edge.Set.top) { + expansionTop = Int(safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top) } - var bottomPx = 0 - if edges.contains(.bottom) { - bottomPx = Int(safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom) - safeBottom = safeArea.presentationBoundsPx.bottom + var expansionBottom = 0 + if expandInto.contains(Edge.Set.bottom) && edges.contains(Edge.Set.bottom) { + expansionBottom = Int(safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom) } - var leadingPx = 0 - if edges.contains(.leading) { - if LocalLayoutDirection.current == androidx.compose.ui.unit.LayoutDirection.Rtl { - leadingPx = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right) - safeRight = safeArea.presentationBoundsPx.right - } else { - leadingPx = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left) - safeLeft = safeArea.presentationBoundsPx.left + var expansionLeft = 0 + var expansionRight = 0 + let isRTL = LocalLayoutDirection.current == androidx.compose.ui.unit.LayoutDirection.Rtl + if isRTL { + if expandInto.contains(Edge.Set.leading) && edges.contains(Edge.Set.leading) { + expansionRight = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right) } - } - var trailingPx = 0 - if edges.contains(.trailing) { - if LocalLayoutDirection.current == androidx.compose.ui.unit.LayoutDirection.Rtl { - trailingPx = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left) - safeLeft = safeArea.presentationBoundsPx.left - } else { - trailingPx = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right) - safeRight = safeArea.presentationBoundsPx.right + if expandInto.contains(Edge.Set.trailing) && edges.contains(Edge.Set.trailing) { + expansionLeft = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left) + } + } else { + if expandInto.contains(Edge.Set.leading) && edges.contains(Edge.Set.leading) { + expansionLeft = Int(safeArea.safeBoundsPx.left - safeArea.presentationBoundsPx.left) + } + if expandInto.contains(Edge.Set.trailing) && edges.contains(Edge.Set.trailing) { + expansionRight = Int(safeArea.presentationBoundsPx.right - safeArea.safeBoundsPx.right) } } + var (safeLeft, safeTop, safeRight, safeBottom) = safeArea.safeBoundsPx + safeLeft -= expansionLeft + safeTop -= expansionTop + safeRight += expansionRight + safeBottom += expansionBottom + let contentSafeBounds = Rect(top: safeTop, left: safeLeft, bottom: safeBottom, right: safeRight) let contentSafeArea = SafeArea(presentation: safeArea.presentationBoundsPx, safe: contentSafeBounds, absoluteSystemBars: safeArea.absoluteSystemBarEdges) EnvironmentValues.shared.setValues { $0.set_safeArea(contentSafeArea) } in: { - Layout(content: { - target(context) + Layout(modifier: modifier.onGloballyPositionedInWindow { + let edges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: expandInto.union(checkEdges)) + edgesState.value = edges + }, content: { + let expansion = IntRect(top: expansionTop, left: expansionLeft, bottom: expansionBottom, right: expansionRight) + target(expansion, edges.intersection(checkEdges)) }) { measurables, constraints in guard !measurables.isEmpty() else { return layout(width: 0, height: 0) {} } - let updatedConstraints = constraints.copy(maxWidth: constraints.maxWidth + leadingPx + trailingPx, maxHeight: constraints.maxHeight + topPx + bottomPx) + let updatedConstraints = constraints.copy(maxWidth: constraints.maxWidth + expansionLeft + expansionRight, maxHeight: constraints.maxHeight + expansionTop + expansionBottom) let targetPlaceables = measurables.map { $0.measure(updatedConstraints) } layout(width: targetPlaceables[0].width, height: targetPlaceables[0].height) { // Layout will center extra space by default - let relativeTopPx = topPx - ((topPx + bottomPx) / 2) - let relativeLeadingPx = leadingPx - ((leadingPx + trailingPx) / 2) + let relativeTop = expansionTop - ((expansionTop + expansionBottom) / 2) + let expansionLeading = isRTL ? expansionRight : expansionLeft + let relativeLeading = expansionLeading - ((expansionLeft + expansionRight) / 2) for targetPlaceable in targetPlaceables { - targetPlaceable.placeRelative(x = -relativeLeadingPx, y = -relativeTopPx) + targetPlaceable.placeRelative(x = -relativeLeading, y = -relativeTop) } } } } } +private func adjacentSafeAreaEdges(bounds: Rect, safeArea: SafeArea, isRTL: Bool, checkEdges: Edge.Set) -> Edge.Set { + var edges: Edge.Set = [] + if checkEdges.contains(Edge.Set.top), bounds.top <= safeArea.safeBoundsPx.top + 0.1 { + edges.insert(Edge.Set.top) + } + if checkEdges.contains(Edge.Set.bottom), bounds.bottom >= safeArea.safeBoundsPx.bottom - 0.1 { + edges.insert(Edge.Set.bottom) + } + if isRTL { + if checkEdges.contains(Edge.Set.leading), bounds.right >= safeArea.safeBoundsPx.right - 0.1 { + edges.insert(Edge.Set.leading) + } + if checkEdges.contains(Edge.Set.trailing), bounds.left <= safeArea.safeBoundsPx.left + 0.1 { + edges.insert(Edge.Set.trailing) + } + } else { + if checkEdges.contains(Edge.Set.leading), bounds.left <= safeArea.safeBoundsPx.left + 0.1 { + edges.insert(Edge.Set.leading) + } + if checkEdges.contains(Edge.Set.trailing), bounds.right >= safeArea.safeBoundsPx.right - 0.1 { + edges.insert(Edge.Set.trailing) + } + } + return edges +} + /// Layout the given view with the given padding. @Composable func PaddingLayout(view: View, padding: EdgeInsets, context: ComposeContext) { PaddingLayout(padding: padding, context: context) { view.Compose($0) } } @Composable func PaddingLayout(padding: EdgeInsets, context: ComposeContext, target: @Composable (ComposeContext) -> Void) { - let density = LocalDensity.current - let topPx = with(density) { padding.top.dp.roundToPx() } - let bottomPx = with(density) { padding.bottom.dp.roundToPx() } - let leadingPx = with(density) { padding.leading.dp.roundToPx() } - let trailingPx = with(density) { padding.trailing.dp.roundToPx() } - Layout(modifier: context.modifier, content = { - target(context.content()) - }) { measurables, constraints in - guard !measurables.isEmpty() else { - return layout(width: 0, height: 0) {} - } - let updatedConstraints = constraints.copy(minWidth: constraint(constraints.minWidth, subtracting: leadingPx + trailingPx), minHeight: constraint(constraints.minHeight, subtracting: topPx + bottomPx), maxWidth: constraint(constraints.maxWidth, subtracting: leadingPx + trailingPx), maxHeight: constraint(constraints.maxHeight, subtracting: topPx + bottomPx)) - let targetPlaceables = measurables.map { $0.measure(updatedConstraints) } - layout(width: targetPlaceables[0].width + leadingPx + trailingPx, height: targetPlaceables[0].height + topPx + bottomPx) { - for targetPlaceable in targetPlaceables { - targetPlaceable.placeRelative(x: leadingPx, y: topPx) + ComposeContainer(modifier: context.modifier) { modifier in + let density = LocalDensity.current + let topPx = with(density) { padding.top.dp.roundToPx() } + let bottomPx = with(density) { padding.bottom.dp.roundToPx() } + let leadingPx = with(density) { padding.leading.dp.roundToPx() } + let trailingPx = with(density) { padding.trailing.dp.roundToPx() } + Layout(modifier: modifier, content = { + target(context.content()) + }) { measurables, constraints in + guard !measurables.isEmpty() else { + return layout(width: 0, height: 0) {} + } + let updatedConstraints = constraints.copy(minWidth: constraint(constraints.minWidth, subtracting: leadingPx + trailingPx), minHeight: constraint(constraints.minHeight, subtracting: topPx + bottomPx), maxWidth: constraint(constraints.maxWidth, subtracting: leadingPx + trailingPx), maxHeight: constraint(constraints.maxHeight, subtracting: topPx + bottomPx)) + let targetPlaceables = measurables.map { $0.measure(updatedConstraints) } + layout(width: targetPlaceables[0].width + leadingPx + trailingPx, height: targetPlaceables[0].height + topPx + bottomPx) { + for targetPlaceable in targetPlaceables { + targetPlaceable.placeRelative(x: leadingPx, y: topPx) + } } } } diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift index 3ebdbd55..181c9345 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift @@ -7,6 +7,7 @@ #if SKIP import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -65,90 +66,94 @@ public struct LazyVGrid: View { let itemContext = context.content() let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in - // Integrate with our scroll-to-top and ScrollViewReader - let gridState = rememberLazyGridState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0) - let coroutineScope = rememberCoroutineScope() - PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { - coroutineScope.launch { - gridState.animateScrollToItem(0) - } - }) - let scrollToID: (Any) -> Void = { id in - if let itemIndex = factoryContext.value.index(for: id) { + IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in + // Integrate with our scroll-to-top and ScrollViewReader + let gridState = rememberLazyGridState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0) + let coroutineScope = rememberCoroutineScope() + PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { coroutineScope.launch { - if Animation.isInWithAnimation { - gridState.animateScrollToItem(itemIndex) - } else { - gridState.scrollToItem(itemIndex) + gridState.animateScrollToItem(0) + } + }) + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + gridState.animateScrollToItem(itemIndex) + } else { + gridState.scrollToItem(itemIndex) + } } } } - } - PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) - PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: gridState, for: [ToolbarPlacement.bottomBar])) - PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: gridState)) + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + if safeAreaEdges.contains(Edge.Set.bottom) { + PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: gridState, for: [ToolbarPlacement.bottomBar])) + PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: gridState)) + } - LazyVerticalGrid(state: gridState, modifier: modifier, columns: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, contentPadding: EnvironmentValues.shared._contentPadding.asPaddingValues(), userScrollEnabled: isScrollEnabled) { - factoryContext.value.initialize( - startItemIndex: isSearchable ? 1 : 0, - item: { view, _ in - item { - Box(contentAlignment: boxAlignment) { - view.Compose(context: itemContext) + LazyVerticalGrid(state: gridState, modifier: Modifier.fillMaxSize(), columns: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, contentPadding: EnvironmentValues.shared._contentPadding.asPaddingValues(), userScrollEnabled: isScrollEnabled) { + factoryContext.value.initialize( + startItemIndex: isSearchable ? 1 : 0, + item: { view, _ in + item { + Box(contentAlignment: boxAlignment) { + view.Compose(context: itemContext) + } } - } - }, - indexedItems: { range, identifier, _, _, _, _, factory in - let count = range.endExclusive - range.start - let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) } - items(count: count, key: key) { index in - Box(contentAlignment: boxAlignment) { - factory(index + range.start).Compose(context: itemContext) + }, + indexedItems: { range, identifier, _, _, _, _, factory in + let count = range.endExclusive - range.start + let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) } + items(count: count, key: key) { index in + Box(contentAlignment: boxAlignment) { + factory(index + range.start).Compose(context: itemContext) + } } - } - }, - objectItems: { objects, identifier, _, _, _, _, factory in - let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) } - items(count: objects.count, key: key) { index in - Box(contentAlignment: boxAlignment) { - factory(objects[index]).Compose(context: itemContext) + }, + objectItems: { objects, identifier, _, _, _, _, factory in + let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) } + items(count: objects.count, key: key) { index in + Box(contentAlignment: boxAlignment) { + factory(objects[index]).Compose(context: itemContext) + } } - } - }, - objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in - let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) } - items(count: objectsBinding.wrappedValue.count, key: key) { index in - Box(contentAlignment: boxAlignment) { - factory(objectsBinding, index).Compose(context: itemContext) + }, + objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in + let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) } + items(count: objectsBinding.wrappedValue.count, key: key) { index in + Box(contentAlignment: boxAlignment) { + factory(objectsBinding, index).Compose(context: itemContext) + } } - } - }, - sectionHeader: { view in - item(span: { GridItemSpan(maxLineSpan) }) { - Box(contentAlignment: androidx.compose.ui.Alignment.Center) { - view.Compose(context: itemContext) + }, + sectionHeader: { view in + item(span: { GridItemSpan(maxLineSpan) }) { + Box(contentAlignment: androidx.compose.ui.Alignment.Center) { + view.Compose(context: itemContext) + } + } + }, + sectionFooter: { view in + item(span: { GridItemSpan(maxLineSpan) }) { + Box(contentAlignment: androidx.compose.ui.Alignment.Center) { + view.Compose(context: itemContext) + } } } - }, - sectionFooter: { view in + ) + if isSearchable { item(span: { GridItemSpan(maxLineSpan) }) { - Box(contentAlignment: androidx.compose.ui.Alignment.Center) { - view.Compose(context: itemContext) - } + let modifier = Modifier.padding(start: 16.dp, end: 16.dp, top: 16.dp, bottom: 8.dp).fillMaxWidth() + SearchField(state: searchableState!, context: context.content(modifier: modifier)) } } - ) - if isSearchable { - item(span: { GridItemSpan(maxLineSpan) }) { - let modifier = Modifier.padding(start: 16.dp, end: 16.dp, top: 16.dp, bottom: 8.dp).fillMaxWidth() - SearchField(state: searchableState!, context: context.content(modifier: modifier)) - } - } - for (view, level) in collectingComposer.views { - if let factory = view as? LazyItemFactory { - factory.composeLazyItems(context: factoryContext.value, level: level) - } else { - factoryContext.value.item(view, level) + for (view, level) in collectingComposer.views { + if let factory = view as? LazyItemFactory { + factory.composeLazyItems(context: factoryContext.value, level: level) + } else { + factoryContext.value.item(view, level) + } } } } diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift index 63ab6047..e9de8494 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift @@ -7,6 +7,7 @@ #if SKIP import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -60,31 +61,33 @@ public struct LazyVStack : View { let itemContext = context.content() let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in - // Integrate with our scroll-to-top and ScrollViewReader - let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0) - let coroutineScope = rememberCoroutineScope() - PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { - coroutineScope.launch { - listState.animateScrollToItem(0) - } - }) - let scrollToID: (Any) -> Void = { id in - if let itemIndex = factoryContext.value.index(for: id) { + IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in + // Integrate with our scroll-to-top and ScrollViewReader + let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable ? 1 : 0) + let coroutineScope = rememberCoroutineScope() + PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { coroutineScope.launch { - if Animation.isInWithAnimation { - listState.animateScrollToItem(itemIndex) - } else { - listState.scrollToItem(itemIndex) + listState.animateScrollToItem(0) + } + }) + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + listState.animateScrollToItem(itemIndex) + } else { + listState.scrollToItem(itemIndex) + } } } } - } - PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) - PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: listState, for: [ToolbarPlacement.bottomBar])) - PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: listState)) + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + if safeAreaEdges.contains(Edge.Set.bottom) { + PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: listState, for: [ToolbarPlacement.bottomBar])) + PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: listState)) + } - Box(modifier: modifier) { - LazyColumn(state: listState, modifier: Modifier.fillMaxWidth(), verticalArrangement: columnArrangement, horizontalAlignment: columnAlignment, contentPadding: EnvironmentValues.shared._contentPadding.asPaddingValues(), userScrollEnabled: isScrollEnabled) { + LazyColumn(state: listState, modifier: Modifier.fillMaxSize(), verticalArrangement: columnArrangement, horizontalAlignment: columnAlignment, contentPadding: EnvironmentValues.shared._contentPadding.asPaddingValues(), userScrollEnabled: isScrollEnabled) { factoryContext.value.initialize( startItemIndex: isSearchable ? 1 : 0, item: { view, _ in diff --git a/Sources/SkipUI/SkipUI/Containers/List.swift b/Sources/SkipUI/SkipUI/Containers/List.swift index 6ed66b51..229f3870 100644 --- a/Sources/SkipUI/SkipUI/Containers/List.swift +++ b/Sources/SkipUI/SkipUI/Containers/List.swift @@ -64,7 +64,7 @@ import struct CoreGraphics.CGFloat #endif /// Corner radius for list sections. -let listSectionnCornerRadius = 8.0 +let listSectionCornerRadius = 8.0 // SKIP INSERT: @Stable // Otherwise Compose recomposes all internal @Composable funcs because 'this' is unstable public final class List : View { @@ -111,8 +111,8 @@ public final class List : View { let safeArea = EnvironmentValues.shared._safeArea var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom] ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? []) - IgnoresSafeAreaLayout(edges: ignoresSafeAreaEdges, context: context) { context in - ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true, then: Modifier.background(BackgroundColor(styling: styling, isItem: false))) { modifier in + ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true, then: Modifier.background(BackgroundColor(styling: styling, isItem: false))) { modifier in + IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier) { safeAreaExpansion, safeAreaEdges in let containerModifier: Modifier let refreshing = remember { mutableStateOf(false) } let refreshAction = EnvironmentValues.shared.refresh @@ -134,9 +134,9 @@ public final class List : View { } Box(modifier: containerModifier) { let density = LocalDensity.current - let headerSafeAreaHeight = headerSafeAreaHeight(safeArea, density: density) - let footerSafeAreaHeight = footerSafeAreaHeight(safeArea, density: density) - ComposeList(context: itemContext, styling: styling, headerSafeAreaHeight: headerSafeAreaHeight, footerSafeAreaHeight: footerSafeAreaHeight) + let headerSafeAreaHeight = with(density) { safeAreaExpansion.top.toDp() } + let footerSafeAreaHeight = with(density) { safeAreaExpansion.bottom.toDp() } + ComposeList(context: itemContext, styling: styling, arguments: ListArguments(headerSafeAreaHeight: headerSafeAreaHeight, footerSafeAreaHeight: footerSafeAreaHeight, safeAreaEdges: safeAreaEdges)) if let refreshState { PullRefreshIndicator(refreshing.value, refreshState, Modifier.align(androidx.compose.ui.Alignment.TopCenter)) } @@ -146,7 +146,7 @@ public final class List : View { } // SKIP INSERT: @OptIn(ExperimentalFoundationApi::class) - @Composable private func ComposeList(context: ComposeContext, styling: ListStyling, headerSafeAreaHeight: Dp, footerSafeAreaHeight: Dp) { + @Composable private func ComposeList(context: ComposeContext, styling: ListStyling, arguments: ListArguments) { // Collect all top-level views to compose. The LazyColumn itself is not a composable context, so we have to execute // our content's Compose function to collect its views before entering the LazyColumn body, then use LazyColumn's // LazyListScope functions to compose individual items @@ -162,18 +162,18 @@ public final class List : View { if styling.style != .plain { modifier = modifier.padding(start: Self.horizontalInset.dp, end: Self.horizontalInset.dp) } - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxSize() let searchableState = EnvironmentValues.shared._searchableState let isSearchable = searchableState?.isOnNavigationStack() == false - let hasHeader = styling.style != ListStyle.plain || (!isSearchable && headerSafeAreaHeight.value > 0) - let hasFooter = styling.style != ListStyle.plain || footerSafeAreaHeight.value > 0 + let hasHeader = styling.style != ListStyle.plain || (!isSearchable && arguments.headerSafeAreaHeight.value > 0) + let hasFooter = styling.style != ListStyle.plain || arguments.footerSafeAreaHeight.value > 0 // Remember the factory because we use it in the remembered reorderable state let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } let moveTrigger = remember { mutableStateOf(0) } - let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable && headerSafeAreaHeight.value <= 0 ? 1 : 0) + let listState = rememberLazyListState(initialFirstVisibleItemIndex = isSearchable && arguments.headerSafeAreaHeight.value <= 0 ? 1 : 0) let reorderableState = rememberReorderableLazyListState(listState: listState, onMove: { from, to in // Trigger recompose on move, but don't read the trigger state until we're inside the list content to limit its scope factoryContext.value.move(from: from.index, to: to.index, trigger: { moveTrigger.value = $0 }) @@ -204,8 +204,16 @@ public final class List : View { } PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) let isSystemBackground = styling.style != ListStyle.plain && styling.backgroundVisibility != Visibility.hidden - PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.navigationBar, ToolbarPlacement.bottomBar])) - PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState)) + // We contribute top bar preferences even without knowing we're safe area-adjacent for multiple reasons: + // - When there is a search bar we may not be adjacent to the top safe area, but we should act like it + // - An expanding nav bar can causes issues detecting safe area adjacency + // - It is unlikely that anyone will use a grouped-style list that is not top-bar adjacent, so the top + // bar should always have the grouped-style system color + PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.navigationBar])) + if arguments.safeAreaEdges.contains(Edge.Set.bottom) { + PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState, for: [ToolbarPlacement.bottomBar])) + PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(isSystemBackground: isSystemBackground, scrollableState: listState)) + } // List item animations in Compose work by setting the `animateItemPlacement` modifier on the items. Critically, // this must be done when the items are composed *prior* to any animated change. So by default we compose all items @@ -318,13 +326,13 @@ public final class List : View { if isSearchable { item { - ComposeSearchField(state: searchableState!, context: context, styling: styling, safeAreaHeight: headerSafeAreaHeight) + ComposeSearchField(state: searchableState!, context: context, styling: styling, safeAreaHeight: arguments.headerSafeAreaHeight) } } if hasHeader { let hasTopSection = collectingComposer.views.firstOrNull() is LazySectionHeader item { - ComposeHeader(styling: styling, safeAreaHeight: isSearchable ? 0.dp : headerSafeAreaHeight, hasTopSection: hasTopSection) + ComposeHeader(styling: styling, safeAreaHeight: isSearchable ? 0.dp : arguments.headerSafeAreaHeight, hasTopSection: hasTopSection) } } for (view, level) in collectingComposer.views { @@ -337,26 +345,12 @@ public final class List : View { if hasFooter { let hasBottomSection = collectingComposer.views.lastOrNull() is LazySectionFooter item { - ComposeFooter(styling: styling, safeAreaHeight: footerSafeAreaHeight, hasBottomSection: hasBottomSection) + ComposeFooter(styling: styling, safeAreaHeight: arguments.footerSafeAreaHeight, hasBottomSection: hasBottomSection) } } } } - - private func headerSafeAreaHeight(_ safeArea: SafeArea?, density: Density) -> Dp { - guard let safeArea, safeArea.absoluteSystemBarEdges.contains(.top) && safeArea.safeBoundsPx.top > safeArea.presentationBoundsPx.top else { - return 0.dp - } - return with(density) { (safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top).toDp() } - } - - private func footerSafeAreaHeight(_ safeArea: SafeArea?, density: Density) -> Dp { - guard let safeArea, safeArea.absoluteSystemBarEdges.contains(.bottom) && safeArea.presentationBoundsPx.bottom > safeArea.safeBoundsPx.bottom else { - return 0.dp - } - return with(density) { (safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom).toDp() } - } - + private static let horizontalInset = 16.0 private static let verticalInset = 16.0 private static let minimumItemHeight = 32.0 @@ -587,10 +581,10 @@ public final class List : View { roundedRectPath.addRoundRect(roundRect) addPath(combine(PathOperation.Difference, rectPath, roundedRectPath)) } - let offset = isTop ? listSectionnCornerRadius.dp : -listSectionnCornerRadius.dp + let offset = isTop ? listSectionCornerRadius.dp : -listSectionCornerRadius.dp let modifier = Modifier .fillMaxWidth() - .height(listSectionnCornerRadius.dp) + .height(listSectionCornerRadius.dp) .offset(y: offset) .clip(shape) .background(fill) @@ -713,6 +707,12 @@ final class ListItemComposer: RenderingComposer { } } } + +@Stable struct ListArguments: Equatable { + let headerSafeAreaHeight: Dp + let footerSafeAreaHeight: Dp + let safeAreaEdges: Edge.Set +} #endif public struct ListStyle: RawRepresentable, Equatable { diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index 26d66a66..0efd6072 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -69,7 +69,6 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -133,7 +132,7 @@ public struct NavigationStack : View where Root: View { // When we layout, only extend into safe areas that are due to system bars, not into any app chrome var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom] ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? []) - IgnoresSafeAreaLayout(edges: ignoresSafeAreaEdges, context: context) { context in + IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges) { _, _ in ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in let isRTL = EnvironmentValues.shared.layoutDirection == LayoutDirection.rightToLeft NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: modifier) { @@ -301,11 +300,8 @@ public struct NavigationStack : View where Root: View { .clickable(interactionSource: interactionSource, indication: nil, onClick: { scrollToTop.value.reduced() }) - .onGloballyPositioned { - let bottomPx = $0.boundsInWindow().bottom - if bottomPx > Float(0.0) { // Sometimes we see random 0 values - topBarBottomPx.value = bottomPx - } + .onGloballyPositionedInWindow { + topBarBottomPx.value = $0.bottom } if !topBarHasColorScheme || isOverlapped, let topBarBackgroundForBrush { let opacity = topBarHasColorScheme ? 1.0 : isInlineTitleDisplayMode ? min(1.0, Double(scrollBehavior.state.overlappedFraction * 5)) : Double(scrollBehavior.state.collapsedFraction) @@ -428,12 +424,9 @@ public struct NavigationStack : View where Root: View { $0.set_placement(placement.union(ViewPlacement.toolbar)) } in: { var bottomBarModifier = Modifier.zIndex(Float(1.1)) - .onGloballyPositioned { - let bounds = $0.boundsInWindow() - if bounds.top > Float(0.0) { // Sometimes we see random 0 values - bottomBarTopPx.value = bounds.top - bottomBarHeightPx.value = bounds.bottom - bounds.top - } + .onGloballyPositionedInWindow { bounds in + bottomBarTopPx.value = bounds.top + bottomBarHeightPx.value = bounds.bottom - bounds.top } if showScrolledBackground, let bottomBarBackgroundForBrush { if let bottomBarBackgroundBrush = bottomBarBackgroundForBrush.asBrush(opacity: 1.0, animationContext: nil) { diff --git a/Sources/SkipUI/SkipUI/Containers/PresentationRoot.swift b/Sources/SkipUI/SkipUI/Containers/PresentationRoot.swift index 2fa33886..ebf13ee1 100644 --- a/Sources/SkipUI/SkipUI/Containers/PresentationRoot.swift +++ b/Sources/SkipUI/SkipUI/Containers/PresentationRoot.swift @@ -29,7 +29,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -60,8 +59,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection rootModifier = rootModifier.imePadding() } rootModifier = rootModifier.background(Color.background.colorImpl()) - .onGloballyPositioned { - presentationBounds.value = $0.boundsInWindow() + .onGloballyPositionedInWindow { + presentationBounds.value = $0 } Box(modifier: rootModifier) { guard presentationBounds.value != Rect.Zero else { diff --git a/Sources/SkipUI/SkipUI/Containers/ScrollView.swift b/Sources/SkipUI/SkipUI/Containers/ScrollView.swift index d11ef322..a295b5d9 100644 --- a/Sources/SkipUI/SkipUI/Containers/ScrollView.swift +++ b/Sources/SkipUI/SkipUI/Containers/ScrollView.swift @@ -10,6 +10,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -64,9 +66,6 @@ public struct ScrollView : View { } }) } - PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: scrollState, for: [ToolbarPlacement.bottomBar])) - PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: scrollState, for: [ToolbarPlacement.bottomBar])) - PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: scrollState)) } if isHorizontalScroll { scrollModifier = scrollModifier.horizontalScroll(scrollState) @@ -74,44 +73,56 @@ public struct ScrollView : View { } let contentContext = context.content() ComposeContainer(scrollAxes: effectiveScrollAxes, modifier: context.modifier, fillWidth: axes.contains(.horizontal), fillHeight: axes.contains(.vertical)) { modifier in - let containerModifier: Modifier - let refreshing = remember { mutableStateOf(false) } - let refreshAction = EnvironmentValues.shared.refresh - let refreshState: PullRefreshState? - if let refreshAction { - let updatedAction = rememberUpdatedState(refreshAction) - refreshState = rememberPullRefreshState(refreshing.value, { - coroutineScope.launch { - refreshing.value = true - updatedAction.value() - refreshing.value = false + IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in + var containerModifier: Modifier = Modifier + if isVerticalScroll { + containerModifier = containerModifier.fillMaxHeight() + if safeAreaEdges.contains(Edge.Set.bottom) { + PreferenceValues.shared.contribute(context: context, key: ToolbarPreferenceKey.self, value: ToolbarPreferences(scrollableState: scrollState, for: [ToolbarPlacement.bottomBar])) + PreferenceValues.shared.contribute(context: context, key: TabBarPreferenceKey.self, value: ToolbarBarPreferences(scrollableState: scrollState)) } - }) - containerModifier = modifier.pullRefresh(refreshState!) - } else { - refreshState = nil - containerModifier = modifier - } + } + if isHorizontalScroll { + containerModifier = containerModifier.fillMaxWidth() + } - Box(modifier: containerModifier) { - Column(modifier: scrollModifier) { - if isVerticalScroll { - let searchableState = EnvironmentValues.shared._searchableState - let isSearchable = searchableState?.isModifierOnNavigationStack == false - if isSearchable { - SearchField(state: searchableState, context: context.content(modifier: Modifier.padding(horizontal: 16.dp, vertical: 8.dp))) + let refreshing = remember { mutableStateOf(false) } + let refreshAction = EnvironmentValues.shared.refresh + let refreshState: PullRefreshState? + if let refreshAction { + let updatedAction = rememberUpdatedState(refreshAction) + refreshState = rememberPullRefreshState(refreshing.value, { + coroutineScope.launch { + refreshing.value = true + updatedAction.value() + refreshing.value = false } - } - EnvironmentValues.shared.setValues { - $0.set_scrollViewAxes(axes) - } in: { - PreferenceValues.shared.collectPreferences([builtinScrollAxisSetCollector]) { - content.Compose(context: contentContext) + }) + containerModifier = containerModifier.pullRefresh(refreshState!) + } else { + refreshState = nil + } + + Box(modifier: containerModifier) { + Column(modifier: scrollModifier) { + if isVerticalScroll { + let searchableState = EnvironmentValues.shared._searchableState + let isSearchable = searchableState?.isModifierOnNavigationStack == false + if isSearchable { + SearchField(state: searchableState, context: context.content(modifier: Modifier.padding(horizontal: 16.dp, vertical: 8.dp))) + } + } + EnvironmentValues.shared.setValues { + $0.set_scrollViewAxes(axes) + } in: { + PreferenceValues.shared.collectPreferences([builtinScrollAxisSetCollector]) { + content.Compose(context: contentContext) + } } } - } - if let refreshState { - PullRefreshIndicator(refreshing.value, refreshState, Modifier.align(androidx.compose.ui.Alignment.TopCenter)) + if let refreshState { + PullRefreshIndicator(refreshing.value, refreshState, Modifier.align(androidx.compose.ui.Alignment.TopCenter)) + } } } } diff --git a/Sources/SkipUI/SkipUI/Containers/TabView.swift b/Sources/SkipUI/SkipUI/Containers/TabView.swift index 3b6b7d57..1c44bd5f 100644 --- a/Sources/SkipUI/SkipUI/Containers/TabView.swift +++ b/Sources/SkipUI/SkipUI/Containers/TabView.swift @@ -44,7 +44,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -119,12 +118,9 @@ public struct TabView : View { return } var tabBarModifier = Modifier.fillMaxWidth() - .onGloballyPositioned { - let bounds = $0.boundsInWindow() - if bounds.top > Float(0.0) { // Sometimes we see random 0 values - bottomBarTopPx.value = bounds.top - bottomBarHeightPx.value = bounds.bottom - bounds.top - } + .onGloballyPositionedInWindow { bounds in + bottomBarTopPx.value = bounds.top + bottomBarHeightPx.value = bounds.bottom - bounds.top } let tint = EnvironmentValues.shared._tint let hasColorScheme = reducedTabBarPreferences.colorScheme != nil @@ -235,7 +231,7 @@ public struct TabView : View { // tab switches var ignoresSafeAreaEdges: Edge.Set = [.bottom, .top] ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? []) - IgnoresSafeAreaLayout(edges: ignoresSafeAreaEdges, context: context) { context in + IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges) { _, _ in ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in // Don't use a Scaffold: it clips content beyond its bounds and prevents .ignoresSafeArea modifiers from working Column(modifier: modifier.background(Color.background.colorImpl())) { diff --git a/Sources/SkipUI/SkipUI/Containers/Table.swift b/Sources/SkipUI/SkipUI/Containers/Table.swift index ffe7c0ff..8c9bedf3 100644 --- a/Sources/SkipUI/SkipUI/Containers/Table.swift +++ b/Sources/SkipUI/SkipUI/Containers/Table.swift @@ -63,14 +63,12 @@ public final class Table : View where ObjectType: Identifiable : View where ObjectType: Identifiable Dp { - guard let safeArea, safeArea.absoluteSystemBarEdges.contains(.top) && safeArea.safeBoundsPx.top > safeArea.presentationBoundsPx.top else { - return 0.dp - } - return with(density) { (safeArea.safeBoundsPx.top - safeArea.presentationBoundsPx.top).toDp() } - } - - private func footerSafeAreaHeight(_ safeArea: SafeArea?, density: Density) -> Dp { - guard let safeArea, safeArea.absoluteSystemBarEdges.contains(.bottom) && safeArea.presentationBoundsPx.bottom > safeArea.safeBoundsPx.bottom else { - return 0.dp - } - return with(density) { (safeArea.presentationBoundsPx.bottom - safeArea.safeBoundsPx.bottom).toDp() } - } #else public var body: some View { stubView() diff --git a/Sources/SkipUI/SkipUI/Layout/GeometryReader.swift b/Sources/SkipUI/SkipUI/Layout/GeometryReader.swift index 7e0a39b1..d60b94d1 100644 --- a/Sources/SkipUI/SkipUI/Layout/GeometryReader.swift +++ b/Sources/SkipUI/SkipUI/Layout/GeometryReader.swift @@ -13,7 +13,6 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity #endif @@ -27,8 +26,8 @@ public struct GeometryReader : View { #if SKIP @Composable public override func ComposeContent(context: ComposeContext) { let rememberedGlobalFramePx = remember { mutableStateOf(nil) } - Box(modifier: context.modifier.fillSize().onGloballyPositioned { - rememberedGlobalFramePx.value = $0.boundsInRoot() + Box(modifier: context.modifier.fillSize().onGloballyPositionedInRoot { + rememberedGlobalFramePx.value = $0 }) { if let globalFramePx = rememberedGlobalFramePx.value { let proxy = GeometryProxy(globalFramePx: globalFramePx, density: LocalDensity.current) diff --git a/Sources/SkipUI/SkipUI/View/View.swift b/Sources/SkipUI/SkipUI/View/View.swift index f97dd5a3..2b75edb7 100644 --- a/Sources/SkipUI/SkipUI/View/View.swift +++ b/Sources/SkipUI/SkipUI/View/View.swift @@ -549,7 +549,7 @@ extension View { return self } return ComposeModifierView(contentView: self) { view, context in - IgnoresSafeAreaLayout(view: view, edges: edges, context: context) + IgnoresSafeAreaLayout(view: view, context: context, expandInto: edges) } #else return self