Telegram-стиль затемнение сверху в ChatDetailView (iOS < 26)

This commit is contained in:
2026-03-09 18:24:52 +05:00
parent b1f71c43f0
commit fd948991f3
14 changed files with 596 additions and 35 deletions

View File

@@ -27,7 +27,9 @@ struct GlassBackButton: View {
.glassEffect(.regular, in: .circle)
} else {
Circle()
.fill(.ultraThinMaterial)
.fill(.thinMaterial)
.overlay { Circle().strokeBorder(Color.white.opacity(0.18), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.12), radius: 20, y: 8)
}
}
}

View File

@@ -20,10 +20,12 @@ struct GlassCard<Content: View>: View {
content()
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius))
} else {
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
content()
.background {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.ultraThinMaterial)
shape.fill(.thinMaterial)
.overlay { shape.strokeBorder(Color.white.opacity(0.10), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.10), radius: 16, y: 6)
}
}
}

View File

@@ -3,7 +3,7 @@ import SwiftUI
// MARK: - Glass Modifier
//
// iOS 26+: native .glassEffect API
// iOS < 26: .ultraThinMaterial blur
// iOS < 26: .thinMaterial blur + stroke + shadow
struct GlassModifier: ViewModifier {
let cornerRadius: CGFloat
@@ -20,7 +20,9 @@ struct GlassModifier: ViewModifier {
} else {
content
.background {
shape.fill(.ultraThinMaterial)
shape.fill(.thinMaterial)
.overlay { shape.strokeBorder(Color.white.opacity(0.10), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.10), radius: 16, y: 6)
}
}
}
@@ -44,7 +46,9 @@ extension View {
}
} else {
background {
Capsule().fill(.ultraThinMaterial)
Capsule().fill(.thinMaterial)
.overlay { Capsule().strokeBorder(Color.white.opacity(0.10), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.10), radius: 16, y: 6)
}
}
}

View File

@@ -3,14 +3,14 @@ import SwiftUI
// MARK: - Glass Navigation Bar Modifier
/// iOS 26+: native glassmorphism (no explicit background needed).
/// iOS < 26: solid adaptive background.
/// iOS < 26: frosted glass material (Telegram-style).
struct GlassNavBarModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content
} else {
content
.toolbarBackground(RosettaColors.Adaptive.background, for: .navigationBar)
.toolbarBackground(.regularMaterial, for: .navigationBar)
}
}
}

View File

@@ -134,13 +134,14 @@ struct RosettaTabBar: View {
}
.background {
if #available(iOS 26.0, *) {
// iOS 26+ liquid glass material for the capsule pill
// iOS 26+ native liquid glass
Capsule()
.fill(.ultraThinMaterial)
.fill(.clear)
.glassEffect(.regular, in: .capsule)
} else {
// iOS < 26 solid dark capsule
// iOS < 26 frosted glass material (Telegram-style)
Capsule()
.fill(TabBarColors.pillBackground)
.fill(.regularMaterial)
.overlay(
Capsule()
.strokeBorder(TabBarColors.pillBorder, lineWidth: 0.5)
@@ -181,11 +182,14 @@ struct RosettaTabBar: View {
Group {
if #available(iOS 26.0, *) {
Capsule().fill(.thinMaterial)
// iOS 26+ native liquid glass
Capsule().fill(.clear)
.glassEffect(.regular, in: .capsule)
.frame(width: width)
.offset(x: xOffset)
} else {
Capsule().fill(TabBarColors.selectionBackground)
// iOS < 26 thin frosted glass
Capsule().fill(.thinMaterial)
.frame(width: width - 4)
.padding(.vertical, 4)
.offset(x: xOffset)

View File

@@ -89,21 +89,22 @@ struct ChatDetailView: View {
private var content: some View {
GeometryReader { geometry in
ZStack {
RosettaColors.Adaptive.background.ignoresSafeArea()
tiledChatBackground
.ignoresSafeArea()
messagesList(maxBubbleWidth: max(min(geometry.size.width * 0.72, 380), 140))
}
.overlay { chatEdgeGradients }
.safeAreaInset(edge: .bottom, spacing: 0) { composer }
.background {
ZStack {
RosettaColors.Adaptive.background
tiledChatBackground
}
.ignoresSafeArea()
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) // скрываем стандартный back, но НЕ навбар
.navigationBarBackButtonHidden(true)
.enableSwipeBack()
.toolbarBackground(.visible, for: .navigationBar)
.applyGlassNavBar()
.toolbar { chatDetailToolbar } // твой header тут
.modifier(ChatDetailNavBarStyleModifier())
.toolbar { chatDetailToolbar }
.toolbar(.hidden, for: .tabBar)
.task {
isViewActive = true
@@ -158,7 +159,7 @@ private extension ChatDetailView {
}
Text(subtitleText)
.font(.system(size: 11, weight: .regular))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(
isTyping || (dialog?.isOnline == true)
? RosettaColors.online
@@ -216,6 +217,39 @@ private extension ChatDetailView {
RosettaColors.adaptive(light: Color(hex: 0x2C2C2E), dark: Color(hex: 0x2C2C2E))
}
// MARK: - Edge Gradients (Telegram-style)
/// Top: native SwiftUI Material blur with gradient mask blurs content behind it.
@ViewBuilder
var chatEdgeGradients: some View {
if #available(iOS 26, *) {
EmptyView()
} else {
VStack(spacing: 0) {
// Telegram-style: dark gradient that smoothly fades content into
// the dark background behind the nav bar pills.
// NOT a material blur Telegram uses dark overlay, not light material.
LinearGradient(
stops: [
.init(color: Color.black.opacity(0.85), location: 0.0),
.init(color: Color.black.opacity(0.75), location: 0.2),
.init(color: Color.black.opacity(0.55), location: 0.4),
.init(color: Color.black.opacity(0.3), location: 0.6),
.init(color: Color.black.opacity(0.12), location: 0.78),
.init(color: Color.black.opacity(0.0), location: 1.0),
],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 90)
Spacer()
}
.ignoresSafeArea(edges: .top)
.allowsHitTesting(false)
}
}
/// Tiled chat background with properly scaled tiles (200pt wide)
private var tiledChatBackground: some View {
Group {
@@ -538,7 +572,26 @@ private extension ChatDetailView {
.animation(composerAnimation, value: shouldShowSendButton)
.animation(composerAnimation, value: isInputFocused)
}
.background(Color.clear)
.background {
if #available(iOS 26, *) {
Color.clear
} else {
// Telegram-style: dark gradient below composer home indicator
VStack(spacing: 0) {
Spacer()
LinearGradient(
colors: [
Color.black.opacity(0.0),
Color.black.opacity(0.55)
],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 34)
}
.ignoresSafeArea(edges: .bottom)
}
}
}
// MARK: - Bubbles / Glass
@@ -575,7 +628,7 @@ private extension ChatDetailView {
func glass(
shape: ChatGlassShape,
strokeOpacity: Double = 0.18,
strokeColor: Color = RosettaColors.Adaptive.border
strokeColor: Color = .white
) -> some View {
if #available(iOS 26.0, *) {
switch shape {
@@ -589,14 +642,21 @@ private extension ChatDetailView {
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: radius, style: .continuous))
}
} else {
// iOS < 26: frosted glass with stroke + shadow (Figma spec)
switch shape {
case .capsule:
Capsule().fill(.ultraThinMaterial)
Capsule().fill(.thinMaterial)
.overlay { Capsule().strokeBorder(strokeColor.opacity(strokeOpacity), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.12), radius: 20, y: 8)
case .circle:
Circle().fill(.ultraThinMaterial)
Circle().fill(.thinMaterial)
.overlay { Circle().strokeBorder(strokeColor.opacity(strokeOpacity), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.12), radius: 20, y: 8)
case let .rounded(radius):
RoundedRectangle(cornerRadius: radius, style: .continuous)
.fill(.ultraThinMaterial)
let r = RoundedRectangle(cornerRadius: radius, style: .continuous)
r.fill(.thinMaterial)
.overlay { r.strokeBorder(strokeColor.opacity(strokeOpacity), lineWidth: 0.5) }
.shadow(color: .black.opacity(0.12), radius: 20, y: 8)
}
}
}
@@ -725,6 +785,22 @@ private extension ChatDetailView {
}()
}
// MARK: - Nav Bar Style (ChatDetail-specific)
/// ChatDetail uses a transparent nav bar so glass/blur pills float independently.
/// ChatListView & SettingsView keep `.applyGlassNavBar()` with `.regularMaterial`.
private struct ChatDetailNavBarStyleModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content
.toolbarBackground(.hidden, for: .navigationBar)
} else {
content
.toolbarBackground(.hidden, for: .navigationBar)
}
}
}
// MARK: - Button Styles
private struct ChatDetailGlassPressButtonStyle: ButtonStyle {

View File

@@ -64,6 +64,7 @@ struct MainTabView: View {
GeometryReader { geometry in
tabPager(availableSize: geometry.size)
}
.ignoresSafeArea()
if !isChatSearchActive && !isAnyChatDetailPresented {
RosettaTabBar(