Telegram-стиль затемнение сверху в ChatDetailView (iOS < 26)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -64,6 +64,7 @@ struct MainTabView: View {
|
||||
GeometryReader { geometry in
|
||||
tabPager(availableSize: geometry.size)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
|
||||
if !isChatSearchActive && !isAnyChatDetailPresented {
|
||||
RosettaTabBar(
|
||||
|
||||
Reference in New Issue
Block a user