import SwiftUI #if canImport(UIKit) import UIKit #endif struct CategoryHeaderOffsetPreferenceKey: PreferenceKey { static let defaultValue: [String: CGFloat] = [:] static func reduce(value: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) { value.merge(nextValue(), uniquingKeysWith: { _, new in new }) } } struct ScrollOffsetReader: View { @Binding var offsetY: CGFloat var body: some View { #if canImport(UIKit) ScrollOffsetReaderRepresentable(offsetY: $offsetY) #else Color.clear #endif } } #if canImport(UIKit) struct ScrollOffsetReaderRepresentable: UIViewRepresentable { @Binding var offsetY: CGFloat func makeUIView(context: Context) -> OffsetProbeView { let view = OffsetProbeView() view.onOffsetChanged = { value in if offsetY != value { offsetY = value } } return view } func updateUIView(_ uiView: OffsetProbeView, context: Context) { uiView.onOffsetChanged = { value in if offsetY != value { offsetY = value } } } } final class OffsetProbeView: UIView { var onOffsetChanged: ((CGFloat) -> Void)? private var observation: NSKeyValueObservation? private weak var observedScrollView: UIScrollView? override func didMoveToWindow() { super.didMoveToWindow() attachIfNeeded() } override func didMoveToSuperview() { super.didMoveToSuperview() attachIfNeeded() } private func attachIfNeeded() { guard observation == nil else { return } guard observedScrollView == nil else { return } if let scrollView = enclosingScrollView() ?? findScrollViewInWindow() { observe(scrollView) return } retryAttach() } private func observe(_ scrollView: UIScrollView) { observedScrollView = scrollView observation = scrollView.observe(\.contentOffset, options: [.new, .initial]) { [weak self, weak scrollView] _, change in guard let self, let scrollView, let y = change.newValue?.y else { return } let adjusted = max(0, y + scrollView.adjustedContentInset.top) DispatchQueue.main.async { self.onOffsetChanged?(adjusted) } } } private func retryAttach() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in self?.attachIfNeeded() } } private func enclosingScrollView() -> UIScrollView? { var current: UIView? = self while let view = current { if let scrollView = view as? UIScrollView { return scrollView } current = view.superview } return nil } private func findScrollViewInWindow() -> UIScrollView? { guard let window else { return nil } let targetPoint = convert(CGPoint(x: bounds.midX, y: bounds.midY), to: window) return findScrollView(in: window, containing: targetPoint) } private func findScrollView(in root: UIView, containing point: CGPoint) -> UIScrollView? { for subview in root.subviews.reversed() { if let match = findScrollView(in: subview, containing: point) { return match } } if let scrollView = root as? UIScrollView { let rectInWindow = scrollView.convert(scrollView.bounds, to: window) if rectInWindow.contains(point) { return scrollView } } return nil } deinit { observation?.invalidate() } } #endif struct AsyncStoreImage: View { let imageURL: String? var body: some View { CachedRemoteImage(imageURL: imageURL) { fallback } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.brandSoft) .clipped() } private var fallback: some View { Image("placeholder-product") .resizable() .scaledToFill() } }