Фикс: кастомный header на экране Request Chats — glass chevron, separator, full-width swipe back

This commit is contained in:
2026-04-14 22:33:19 +05:00
parent e5c0a270df
commit 03c556f77e
4 changed files with 1513 additions and 59 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -82,7 +82,7 @@ final class RequestChatsController: UIViewController {
[weak self] cell, indexPath, dialog in [weak self] cell, indexPath, dialog in
guard let self else { return } guard let self else { return }
cell.configure(with: dialog, isSyncing: self.isSyncing) cell.configure(with: dialog, isSyncing: self.isSyncing)
cell.setSeparatorHidden(indexPath.item == 0) cell.setSeparatorHidden(false)
} }
} }

View File

@@ -51,6 +51,8 @@ final class ChatListCollectionController: UIViewController {
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, String>! private var dataSource: UICollectionViewDiffableDataSource<Section, String>!
/// Overscroll background fills rubber-band area with pinnedItemBackgroundColor (Telegram parity)
private let overscrollBackgroundView = UIView()
private var cellRegistration: UICollectionView.CellRegistration<ChatListCell, Dialog>! private var cellRegistration: UICollectionView.CellRegistration<ChatListCell, Dialog>!
private var requestsCellRegistration: UICollectionView.CellRegistration<ChatListRequestsCell, Int>! private var requestsCellRegistration: UICollectionView.CellRegistration<ChatListRequestsCell, Int>!
private let floatingTabBarTotalHeight: CGFloat = 72 private let floatingTabBarTotalHeight: CGFloat = 72
@@ -94,6 +96,11 @@ final class ChatListCollectionController: UIViewController {
applyInsets() applyInsets()
view.addSubview(collectionView) view.addSubview(collectionView)
// Overscroll background behind cells, shows pinnedItemBackgroundColor on pull-down bounce
overscrollBackgroundView.isUserInteractionEnabled = false
overscrollBackgroundView.isHidden = true
collectionView.insertSubview(overscrollBackgroundView, at: 0)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
@@ -155,6 +162,34 @@ final class ChatListCollectionController: UIViewController {
reportPinnedHeaderFraction() reportPinnedHeaderFraction()
} }
private func updateOverscrollBackground() {
let hasPin = !pinnedDialogs.isEmpty
overscrollBackgroundView.isHidden = !hasPin
guard hasPin, collectionView != nil else { return }
let isDark = traitCollection.userInterfaceStyle == .dark
overscrollBackgroundView.backgroundColor = isDark
? UIColor(red: 0x1C / 255, green: 0x1C / 255, blue: 0x1D / 255, alpha: 1)
: UIColor(red: 0xF7 / 255, green: 0xF7 / 255, blue: 0xF7 / 255, alpha: 1)
let offset = collectionView.contentOffset.y
let insetTop = collectionView.contentInset.top
let overscrollAmount = -(offset + insetTop)
let width = collectionView.bounds.width
if overscrollAmount > 0 {
overscrollBackgroundView.frame = CGRect(
x: 0, y: offset,
width: width, height: insetTop + overscrollAmount
)
} else {
overscrollBackgroundView.frame = CGRect(
x: 0, y: -insetTop,
width: width, height: insetTop
)
}
}
private func schedulePinnedHeaderFractionReport(force: Bool = false) { private func schedulePinnedHeaderFractionReport(force: Bool = false) {
if isPinnedFractionReportScheduled { return } if isPinnedFractionReportScheduled { return }
isPinnedFractionReportScheduled = true isPinnedFractionReportScheduled = true
@@ -171,23 +206,33 @@ final class ChatListCollectionController: UIViewController {
collectionView.window != nil, collectionView.window != nil,
!pinnedDialogs.isEmpty else { return 0.0 } !pinnedDialogs.isEmpty else { return 0.0 }
var maxPinnedOffset: CGFloat = 0.0 // Telegram: itemNode.frame is in VISUAL space (changes with scroll).
// UICollectionView cell.frame is in CONTENT space (static).
// Convert: visibleMaxY = cell.frame.maxY - contentOffset.y
let offset = collectionView.contentOffset.y
var maxPinnedVisibleOffset: CGFloat = 0.0
var foundAny = false
for cell in collectionView.visibleCells { for cell in collectionView.visibleCells {
guard let indexPath = collectionView.indexPath(for: cell), guard let indexPath = collectionView.indexPath(for: cell),
sectionForIndexPath(indexPath) == .pinned else { sectionForIndexPath(indexPath) == .pinned else {
continue continue
} }
maxPinnedOffset = max(maxPinnedOffset, cell.frame.maxY) let visibleMaxY = cell.frame.maxY - offset
maxPinnedVisibleOffset = max(maxPinnedVisibleOffset, visibleMaxY)
foundAny = true
} }
if !foundAny { return 0.0 }
let viewportInsetTop = collectionView.contentInset.top let viewportInsetTop = collectionView.contentInset.top
guard viewportInsetTop > 0 else { return 0.0 } guard viewportInsetTop > 0 else { return 0.0 }
if maxPinnedOffset >= viewportInsetTop { if maxPinnedVisibleOffset >= viewportInsetTop {
return 1.0 return 1.0
} }
return max(0.0, min(1.0, maxPinnedOffset / viewportInsetTop)) return max(0.0, min(1.0, maxPinnedVisibleOffset / viewportInsetTop))
} }
private func reportPinnedHeaderFraction(force: Bool = false) { private func reportPinnedHeaderFraction(force: Bool = false) {
@@ -216,15 +261,19 @@ final class ChatListCollectionController: UIViewController {
let section = NSCollectionLayoutSection.list(using: listConfig, layoutEnvironment: environment) let section = NSCollectionLayoutSection.list(using: listConfig, layoutEnvironment: environment)
section.interGroupSpacing = 0 section.interGroupSpacing = 0
// Add pinned section background decoration // Add pinned section background decoration to pinned AND requests sections
// (requests sit above pinned, so they share the same gray background)
if let self, if let self,
sectionIndex < self.dataSource?.snapshot().sectionIdentifiers.count ?? 0, sectionIndex < self.dataSource?.snapshot().sectionIdentifiers.count ?? 0,
self.dataSource?.snapshot().sectionIdentifiers[sectionIndex] == .pinned { !self.pinnedDialogs.isEmpty {
let sectionId = self.dataSource?.snapshot().sectionIdentifiers[sectionIndex]
if sectionId == .pinned || sectionId == .requests {
let bgItem = NSCollectionLayoutDecorationItem.background( let bgItem = NSCollectionLayoutDecorationItem.background(
elementKind: PinnedSectionBackgroundView.elementKind elementKind: PinnedSectionBackgroundView.elementKind
) )
section.decorationItems = [bgItem] section.decorationItems = [bgItem]
} }
}
return section return section
} }
@@ -262,9 +311,8 @@ final class ChatListCollectionController: UIViewController {
requestsCellRegistration = UICollectionView.CellRegistration<ChatListRequestsCell, Int> { requestsCellRegistration = UICollectionView.CellRegistration<ChatListRequestsCell, Int> {
[weak self] cell, _, count in [weak self] cell, _, count in
let hasPinned = !(self?.pinnedDialogs.isEmpty ?? true) // Always show separator under requests row
// Requests row separator should be hidden when pinned section exists. cell.configure(count: count, showBottomSeparator: true)
cell.configure(count: count, showBottomSeparator: !hasPinned)
} }
} }
@@ -353,6 +401,7 @@ final class ChatListCollectionController: UIViewController {
// Notify host immediately so top chrome reacts in the same frame. // Notify host immediately so top chrome reacts in the same frame.
onPinnedStateChange?(!pinned.isEmpty) onPinnedStateChange?(!pinned.isEmpty)
updateOverscrollBackground()
} }
/// Directly reconfigure only visible cells no snapshot rebuild, no animation. /// Directly reconfigure only visible cells no snapshot rebuild, no animation.
@@ -366,7 +415,7 @@ final class ChatListCollectionController: UIViewController {
let typingUsers = typingDialogs[dialog.opponentKey] let typingUsers = typingDialogs[dialog.opponentKey]
chatCell.configure(with: dialog, isSyncing: isSyncing, typingUsers: typingUsers) chatCell.configure(with: dialog, isSyncing: isSyncing, typingUsers: typingUsers)
} else if let reqCell = cell as? ChatListRequestsCell { } else if let reqCell = cell as? ChatListRequestsCell {
reqCell.configure(count: requestsCount, showBottomSeparator: pinnedDialogs.isEmpty) reqCell.configure(count: requestsCount, showBottomSeparator: true)
} }
} }
} }
@@ -474,6 +523,7 @@ extension ChatListCollectionController: UICollectionViewDelegate {
onScrollOffsetChange?(expansion) onScrollOffsetChange?(expansion)
} }
reportPinnedHeaderFraction() reportPinnedHeaderFraction()
updateOverscrollBackground()
} }
func scrollViewWillEndDragging( func scrollViewWillEndDragging(

View File

@@ -66,13 +66,11 @@ private final class ChatListUIKitCoordinator {
} }
func setDetailPresented(_ presented: Bool) { func setDetailPresented(_ presented: Bool) {
DispatchQueue.main.async {
if self.isDetailPresented.wrappedValue != presented { if self.isDetailPresented.wrappedValue != presented {
self.isDetailPresented.wrappedValue = presented self.isDetailPresented.wrappedValue = presented
} }
} }
} }
}
@MainActor @MainActor
final class ChatListRootViewController: UIViewController, UINavigationControllerDelegate { final class ChatListRootViewController: UIViewController, UINavigationControllerDelegate {
@@ -197,9 +195,10 @@ final class ChatListRootViewController: UIViewController, UINavigationController
} }
private func updateNavigationBlurHeight() { private func updateNavigationBlurHeight() {
let headerBottom = headerTotalHeight // Telegram: edgeEffectHeight = componentHeight + 14 - searchBarExpansion
let expandedSearchHeight = searchChromeHeight * lastSearchExpansion // Result: edge effect covers toolbar area + 14pt ONLY (not search bar).
navigationBlurHeightConstraint?.constant = max(0, headerBottom + expandedSearchHeight + 14.0) // Search bar has its own background, doesn't need blur coverage.
navigationBlurHeightConstraint?.constant = max(0, headerTotalHeight + 14.0)
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@@ -604,16 +603,9 @@ final class ChatListRootViewController: UIViewController, UINavigationController
} }
private func openChat(route: ChatRoute) { private func openChat(route: ChatRoute) {
let detail = ChatDetailView( onDetailPresentedChanged?(true) // hide tab bar BEFORE push animation
route: route, let detail = ChatDetailViewController(route: route)
onPresentedChange: { [weak self] presented in navigationController?.pushViewController(detail, animated: true)
self?.onDetailPresentedChanged?(presented)
}
)
let hosting = UIHostingController(rootView: detail.id(route.publicKey))
hosting.view.backgroundColor = UIColor(RosettaColors.Adaptive.background)
navigationController?.pushViewController(hosting, animated: true)
} }
private func openRequests() { private func openRequests() {
@@ -705,9 +697,13 @@ final class ChatListRootViewController: UIViewController, UINavigationController
willShow viewController: UIViewController, willShow viewController: UIViewController,
animated: Bool animated: Bool
) { ) {
// Show standard nav bar for pushed screens, hide on chat list let hideNavBar = viewController === self
let isChatList = viewController === self || viewController is ChatDetailViewController
navigationController.setNavigationBarHidden(isChatList, animated: animated) || viewController is RequestChatsUIKitShellController
navigationController.setNavigationBarHidden(hideNavBar, animated: animated)
let isPresented = navigationController.viewControllers.count > 1
onDetailPresentedChanged?(isPresented)
} }
func navigationController( func navigationController(
@@ -729,6 +725,14 @@ final class RequestChatsUIKitShellController: UIViewController {
private let requestsController = RequestChatsController() private let requestsController = RequestChatsController()
private var observationTask: Task<Void, Never>? private var observationTask: Task<Void, Never>?
// Custom header elements (direct subviews glass needs this)
private let headerBarHeight: CGFloat = 44
private let backButton = UIControl()
private let backGlassBackground = ChatListToolbarGlassCapsuleView()
private let chevronLayer = CAShapeLayer()
private let titleLabel = UILabel()
private var addedFullWidthGesture = false
init(viewModel: ChatListViewModel) { init(viewModel: ChatListViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@@ -741,13 +745,14 @@ final class RequestChatsUIKitShellController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = UIColor(RosettaColors.Adaptive.background) view.backgroundColor = UIColor(RosettaColors.Adaptive.background)
title = "Request Chats"
setupCustomHeader()
addChild(requestsController) addChild(requestsController)
view.addSubview(requestsController.view) view.addSubview(requestsController.view)
requestsController.view.translatesAutoresizingMaskIntoConstraints = false requestsController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
requestsController.view.topAnchor.constraint(equalTo: view.topAnchor), requestsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: headerBarHeight),
requestsController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), requestsController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
requestsController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), requestsController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
requestsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), requestsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
@@ -765,10 +770,117 @@ final class RequestChatsUIKitShellController: UIViewController {
render() render()
} }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupFullWidthSwipeBack()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
layoutCustomHeader()
}
deinit { deinit {
observationTask?.cancel() observationTask?.cancel()
} }
// MARK: - Custom Header
private func setupCustomHeader() {
// Back button glass capsule + SVG chevron (TelegramIconPath.backChevron)
backGlassBackground.translatesAutoresizingMaskIntoConstraints = false
backButton.addSubview(backGlassBackground)
NSLayoutConstraint.activate([
backGlassBackground.topAnchor.constraint(equalTo: backButton.topAnchor),
backGlassBackground.leadingAnchor.constraint(equalTo: backButton.leadingAnchor),
backGlassBackground.trailingAnchor.constraint(equalTo: backButton.trailingAnchor),
backGlassBackground.bottomAnchor.constraint(equalTo: backButton.bottomAnchor),
])
// Chevron from SVG path (viewBox 11×20, same as ChatDetailView)
let viewBox = CGSize(width: 11, height: 20)
let iconSize = CGSize(width: 11, height: 20)
var parser = SVGPathParser(pathData: TelegramIconPath.backChevron)
let rawPath = parser.parse()
let buttonSize: CGFloat = 44
var transform = CGAffineTransform(
scaleX: iconSize.width / viewBox.width,
y: iconSize.height / viewBox.height
).translatedBy(
x: (buttonSize - iconSize.width) / 2 * (viewBox.width / iconSize.width),
y: (buttonSize - iconSize.height) / 2 * (viewBox.height / iconSize.height)
)
chevronLayer.path = rawPath.copy(using: &transform)
chevronLayer.fillColor = UIColor(RosettaColors.Adaptive.text).cgColor
chevronLayer.frame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
backButton.layer.addSublayer(chevronLayer)
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
backButton.layer.zPosition = 55
view.addSubview(backButton)
// Title
titleLabel.text = "Request Chats"
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
titleLabel.textColor = UIColor(RosettaColors.Adaptive.text)
titleLabel.textAlignment = .center
titleLabel.layer.zPosition = 55
view.addSubview(titleLabel)
}
private func layoutCustomHeader() {
let statusBarHeight = view.safeAreaInsets.top
let centerY = statusBarHeight + headerBarHeight * 0.5
let sideInset: CGFloat = 16
let buttonSize: CGFloat = 44
backButton.frame = CGRect(
x: sideInset,
y: centerY - buttonSize * 0.5,
width: buttonSize,
height: buttonSize
)
let titleSize = titleLabel.intrinsicContentSize
titleLabel.frame = CGRect(
x: (view.bounds.width - titleSize.width) * 0.5,
y: centerY - titleSize.height * 0.5,
width: titleSize.width,
height: titleSize.height
)
}
@objc private func backTapped() {
navigationController?.popViewController(animated: true)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
chevronLayer.fillColor = UIColor(RosettaColors.Adaptive.text).cgColor
}
// MARK: - Full-Width Swipe Back
private func setupFullWidthSwipeBack() {
guard !addedFullWidthGesture else { return }
addedFullWidthGesture = true
guard let nav = navigationController,
let edgeGesture = nav.interactivePopGestureRecognizer,
let targets = edgeGesture.value(forKey: "targets") as? NSArray,
targets.count > 0
else { return }
edgeGesture.isEnabled = true
let fullWidthGesture = UIPanGestureRecognizer()
fullWidthGesture.setValue(targets, forKey: "targets")
fullWidthGesture.delegate = self
nav.view.addGestureRecognizer(fullWidthGesture)
}
// MARK: - Observation
private func startObservationLoop() { private func startObservationLoop() {
observationTask?.cancel() observationTask?.cancel()
observationTask = Task { @MainActor [weak self] in observationTask = Task { @MainActor [weak self] in
@@ -797,6 +909,23 @@ final class RequestChatsUIKitShellController: UIViewController {
} }
} }
// MARK: - UIGestureRecognizerDelegate (full-width swipe back)
extension RequestChatsUIKitShellController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return false }
let velocity = pan.velocity(in: pan.view)
return velocity.x > 0 && abs(velocity.x) > abs(velocity.y)
}
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
false
}
}
private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate { private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
var onQueryChanged: ((String) -> Void)? var onQueryChanged: ((String) -> Void)?
@@ -1039,12 +1168,12 @@ private final class ChatListSearchHeaderView: UIView, UITextFieldDelegate {
private final class ChatListHeaderBlurView: UIView { private final class ChatListHeaderBlurView: UIView {
// Tint overlay shows pinned section background color via gradient mask // Telegram EdgeEffectView port:
// 1. CABackdropLayer variable blur of content behind (radius 1.0)
// 2. Tint overlay color changes with pinnedFraction, alpha 0.85, gradient-masked
private let tintView = UIView() private let tintView = UIView()
private let tintMaskView = UIImageView() private let tintMaskView = UIImageView()
// CABackdropLayer captures content behind and applies subtle blur
private var backdropLayer: CALayer? private var backdropLayer: CALayer?
private let fadeMaskLayer = CAGradientLayer()
private var plainBackgroundColor: UIColor = .black private var plainBackgroundColor: UIColor = .black
private var pinnedBackgroundColor: UIColor = .black private var pinnedBackgroundColor: UIColor = .black
private var currentProgress: CGFloat = 0.0 private var currentProgress: CGFloat = 0.0
@@ -1055,7 +1184,7 @@ private final class ChatListHeaderBlurView: UIView {
super.init(frame: frame) super.init(frame: frame)
isUserInteractionEnabled = false isUserInteractionEnabled = false
// Backdrop blur layer very subtle (radius 1.0), no colorMatrix // Backdrop blur layer (Telegram: VariableBlurView maxRadius 1.0)
if let backdrop = BackdropLayerHelper.createBackdropLayer() { if let backdrop = BackdropLayerHelper.createBackdropLayer() {
backdrop.delegate = BackdropLayerDelegate.shared backdrop.delegate = BackdropLayerDelegate.shared
BackdropLayerHelper.setScale(backdrop, scale: 0.5) BackdropLayerHelper.setScale(backdrop, scale: 0.5)
@@ -1067,14 +1196,11 @@ private final class ChatListHeaderBlurView: UIView {
self.backdropLayer = backdrop self.backdropLayer = backdrop
} }
// Tint view with gradient mask (for pinned section color) // Tint overlay with gradient mask (Telegram: contentView at alpha 0.85)
tintView.mask = tintMaskView tintView.mask = tintMaskView
tintView.alpha = 0.85 tintView.alpha = 0.85
addSubview(tintView) addSubview(tintView)
// Gradient fade mask on the whole view
layer.mask = fadeMaskLayer
applyAdaptiveColors() applyAdaptiveColors()
} }
@@ -1093,7 +1219,6 @@ private final class ChatListHeaderBlurView: UIView {
backdropLayer?.frame = bounds backdropLayer?.frame = bounds
tintView.frame = bounds tintView.frame = bounds
tintMaskView.frame = bounds tintMaskView.frame = bounds
updateFadeMask()
updateTintMask() updateTintMask()
} }
@@ -1104,6 +1229,7 @@ private final class ChatListHeaderBlurView: UIView {
updateChromeOpacity() updateChromeOpacity()
} }
// Telegram: plainBackgroundColor.mixedWith(pinnedItemBackgroundColor, alpha: pinnedFraction)
private func updateEdgeEffectColor() { private func updateEdgeEffectColor() {
let effectivePinnedFraction = isSearchCurrentlyActive ? 0.0 : currentPinnedFraction let effectivePinnedFraction = isSearchCurrentlyActive ? 0.0 : currentPinnedFraction
let resolved = plainBackgroundColor.mixedWith(pinnedBackgroundColor, alpha: effectivePinnedFraction) let resolved = plainBackgroundColor.mixedWith(pinnedBackgroundColor, alpha: effectivePinnedFraction)
@@ -1112,8 +1238,7 @@ private final class ChatListHeaderBlurView: UIView {
private func updateChromeOpacity() { private func updateChromeOpacity() {
let clamped = max(0.0, min(1.0, currentProgress)) let clamped = max(0.0, min(1.0, currentProgress))
// Backdrop blur is always present its visibility depends on content behind. // Telegram: content alpha is always 0.85; we modulate by scroll progress
// Tint overlay fades in with scroll progress.
tintView.alpha = 0.85 * clamped tintView.alpha = 0.85 * clamped
} }
@@ -1123,17 +1248,6 @@ private final class ChatListHeaderBlurView: UIView {
tintMaskView.image = VariableBlurEdgeView.generateEdgeGradient(baseHeight: edgeSize) tintMaskView.image = VariableBlurEdgeView.generateEdgeGradient(baseHeight: edgeSize)
} }
private func updateFadeMask() {
let height = max(1, bounds.height)
let fadeHeight = min(54.0, height)
let fadeStart = max(0.0, (height - fadeHeight) / height)
fadeMaskLayer.frame = bounds
fadeMaskLayer.startPoint = CGPoint(x: 0.5, y: 0)
fadeMaskLayer.endPoint = CGPoint(x: 0.5, y: 1)
fadeMaskLayer.colors = [UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor]
fadeMaskLayer.locations = [0, NSNumber(value: Float(fadeStart)), 1]
}
func setProgress(_ progress: CGFloat, pinnedFraction: CGFloat, isSearchActive: Bool) { func setProgress(_ progress: CGFloat, pinnedFraction: CGFloat, isSearchActive: Bool) {
currentProgress = max(0.0, min(1.0, progress)) currentProgress = max(0.0, min(1.0, progress))
currentPinnedFraction = max(0.0, min(1.0, pinnedFraction)) currentPinnedFraction = max(0.0, min(1.0, pinnedFraction))