Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Flipcash/Core/Screens/Profile/ChatProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ struct ChatProfileScreen: View {
if origin == .chat {
Color.clear
.frame(height: 0)
.profilePinnedBackdrop(isActive: scrollFit.overflows)
.profileBarlessFade(isActive: scrollFit.overflows)
} else if conversation != nil {
VStack(spacing: 8) {
openChatButton
Expand Down
18 changes: 18 additions & 0 deletions Flipcash/Core/Screens/Profile/ProfilePinnedBackdrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ extension View {
}
}

/// Fades content into the bottom of a profile that pins no bar, the way the chat's transcript
/// dissolves under its composer: a ramp from the clearance's top edge to the screen's bottom edge.
/// `isActive` false draws nothing, for a screen whose content doesn't scroll.
func profileBarlessFade(isActive: Bool) -> some View {
background {
if isActive {
LinearGradient(
gradient: ChatFade.gradient(Color.backgroundMain),
startPoint: .top,
endPoint: .bottom
)
.padding(.top, -ProfilePinnedBackdrop.fadeHeight)
.ignoresSafeArea(edges: .bottom)
.allowsHitTesting(false)
}
}
}

/// Ends a profile's scroll content above the fade ``profilePinnedBackdrop()`` draws over the
/// pinned bar, so the last row is readable when scrolled to the bottom. Apply to the scroll view;
/// `isActive` false leaves the margin off a screen that draws no backdrop.
Expand Down
3 changes: 2 additions & 1 deletion Flipcash/Core/Screens/Profile/UserProfileScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ private struct UserProfileContent: View {
// On iOS 26 the pinned button joins the bottom scroll edge effect, so content fades under it.
.scrollEdgeBar(.bottom) {
pinnedButton
.profilePinnedBackdrop(isActive: !hidesPinnedBar || scrollFit.overflows)
.profilePinnedBackdrop(isActive: !hidesPinnedBar)
.profileBarlessFade(isActive: hidesPinnedBar && scrollFit.overflows)
// Toasts rise above the button rather than covering it.
.toastClearance(toasts)
}
Expand Down
37 changes: 25 additions & 12 deletions FlipcashUI/Sources/FlipcashUI/Chat/ComposerFade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@
import SwiftUI
import UIKit

/// The chat's bottom dissolve as a ramp of background opacities: clear for most of its height, then
/// a late rise that stops short of opaque, so content behind the bottom edge still shows faintly.
public enum ChatFade {

private static let curve = UnitCurve.bezier(
startControlPoint: UnitPoint(x: 0.42, y: 0.18),
endControlPoint: UnitPoint(x: 0.17, y: 1.04)
)
private static let endOpacity = 0.9
private static let samples = 32

/// The ramp sampled top to bottom, as a location and the background's opacity there.
static let stops: [(location: Double, opacity: Double)] = (0...samples).map {
let location = Double($0) / Double(samples)
return (location, endOpacity * curve.value(at: location))
}

/// The ramp in `color`, for a top-to-bottom gradient.
public static func gradient(_ color: Color) -> Gradient {
Gradient(stops: stops.map { .init(color: color.opacity($0.opacity), location: $0.location) })
}
}

/// The dissolve from the transcript into the bottom of the screen: a ramp from nothing to the chat
/// background, starting at the bar's top edge and reaching full opacity only at the screen's bottom
/// edge, or the keyboard's top edge while one is up. The bar floats over it with no surface of its
Expand All @@ -19,26 +42,16 @@ import UIKit
@MainActor
final class ComposerFadeView: UIView {

/// The ramp's shape: clear for most of its height, then a late rise into the background.
private static let curve = UnitCurve.bezier(
startControlPoint: UnitPoint(x: 0.42, y: 0.18),
endControlPoint: UnitPoint(x: 0.17, y: 1.04)
)
/// Short of opaque, so a bubble behind the bar's bottom edge still shows faintly.
private static let endOpacity = 0.9
private static let samples = 32

private let colorRamp = GradientView()

init() {
super.init(frame: .zero)
isUserInteractionEnabled = false
addSubview(colorRamp)
let base = UIColor(Color.backgroundMain)
let stops = (0...Self.samples).map { Double($0) / Double(Self.samples) }
colorRamp.set(
colors: stops.map { base.withAlphaComponent(Self.endOpacity * Self.curve.value(at: $0)) },
locations: stops.map { NSNumber(value: $0) }
colors: ChatFade.stops.map { base.withAlphaComponent($0.opacity) },
locations: ChatFade.stops.map { NSNumber(value: $0.location) }
)
}

Expand Down
Loading