fix(ios): stabilize stream navigation and header insets

This commit is contained in:
tapframe 2026-07-10 15:40:41 +05:30
parent 5789af84aa
commit 070ef29ede
5 changed files with 44 additions and 8 deletions

View file

@ -1,7 +1,9 @@
package com.nuvio.app.core.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -11,3 +13,7 @@ internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 6.dp
@Composable
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets = WindowInsets.navigationBars
@Composable
internal actual fun platformPhysicalTopInset(): Dp =
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()

View file

@ -13,6 +13,10 @@ internal expect val nuvioBottomNavigationExtraVerticalPadding: Dp
@Composable
internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets
/** Physical display-safe top inset, excluding any enclosing native toolbar. */
@Composable
internal expect fun platformPhysicalTopInset(): Dp
internal val LocalNuvioBottomNavigationOverlayPadding = staticCompositionLocalOf { 0.dp }
@Composable

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.platformPhysicalTopInset
import com.nuvio.app.features.details.MetaDetails
import com.nuvio.app.isIos
import com.nuvio.app.navigation.LocalUseNativeNavigation
@ -55,10 +56,14 @@ fun DetailFloatingHeader(
onToggleSaved: () -> Unit,
modifier: Modifier = Modifier,
) {
val safeAreaTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val useNativeNavigation = LocalUseNativeNavigation.current
val safeAreaTop = if (useNativeNavigation) {
platformPhysicalTopInset()
} else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}
val headerTopPadding = (safeAreaTop - 6.dp).coerceAtLeast(safeAreaTop * 0.8f)
val interactive = progress > 0.05f
val useNativeNavigation = LocalUseNativeNavigation.current
val surfaceColor = backgroundColor ?: if (isIos) {
MaterialTheme.colorScheme.surface.copy(alpha = 1.0f)
} else {

View file

@ -2,11 +2,16 @@ package com.nuvio.app.core.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable
import androidx.compose.ui.uikit.LocalUIViewController
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.useContents
internal actual val nuvioPlatformExtraTopPadding: Dp = 0.dp
internal actual val nuvioPlatformExtraBottomPadding: Dp = 0.dp
@ -15,3 +20,14 @@ internal actual val nuvioBottomNavigationExtraVerticalPadding: Dp = 0.dp
@Composable
internal actual fun nuvioBottomNavigationBarInsets(): WindowInsets =
WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
@OptIn(ExperimentalForeignApi::class)
@Composable
internal actual fun platformPhysicalTopInset(): Dp {
val physicalTop = LocalUIViewController.current.view.window
?.safeAreaInsets
?.useContents { top.toFloat() }
return physicalTop?.dp
?: WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}

View file

@ -596,6 +596,11 @@ struct TabContentView: View {
coordinator: coordinator,
appCoordinator: appCoordinator
)
// A native replace keeps the same NavigationStack depth.
// Keying by the wrapper forces SwiftUI to replace the
// embedded Compose controller instead of reusing the old
// screen with the new route's toolbar preferences.
.id(wrapper.id)
} else {
Color.clear
}
@ -642,12 +647,12 @@ private struct DetailDestinationView: View {
@ObservedObject var coordinator: TabNavigationCoordinator
@ObservedObject var appCoordinator: AppNavigationCoordinator
private var isMetaDetail: Bool {
wrapper.route is DetailRoute
private var usesComposeNavigationHeader: Bool {
wrapper.route is DetailRoute || wrapper.route is StreamRoute
}
private var showsReadabilityFade: Bool {
!wrapper.route.hidesNavigationBar && !isMetaDetail
!wrapper.route.hidesNavigationBar && !usesComposeNavigationHeader
}
private var content: some View {
@ -663,9 +668,9 @@ private struct DetailDestinationView: View {
NativeToolbarReadabilityFade()
}
}
.navigationTitle(isMetaDetail ? "" : wrapper.route.title ?? "")
.navigationTitle(usesComposeNavigationHeader ? "" : wrapper.route.title ?? "")
.navigationBarTitleDisplayMode(.inline)
.toolbarRole(isMetaDetail ? .editor : .automatic)
.toolbarRole(usesComposeNavigationHeader ? .editor : .automatic)
.toolbar(.hidden, for: .tabBar)
.toolbar(
wrapper.route.hidesNavigationBar ? Visibility.hidden : Visibility.visible,
@ -675,7 +680,7 @@ private struct DetailDestinationView: View {
@ViewBuilder
var body: some View {
if #available(iOS 26.0, *), !isMetaDetail {
if #available(iOS 26.0, *), !usesComposeNavigationHeader {
content.navigationSubtitle(wrapper.route.subtitle ?? "")
} else {
content