import SwiftUI #if canImport(UIKit) import UIKit #endif #if os(iOS) @MainActor struct ScrollOffsetObserver: UIViewRepresentable { let onOffsetChange: (CGFloat) -> Void func makeUIView(context: Context) -> ScrollOffsetProbeView { let view = ScrollOffsetProbeView() view.onOffsetChange = onOffsetChange return view } func updateUIView(_ uiView: ScrollOffsetProbeView, context: Context) { uiView.onOffsetChange = onOffsetChange uiView.attachIfNeeded() } } @MainActor final class ScrollOffsetProbeView: UIView { var onOffsetChange: (CGFloat) -> Void = { _ in } private weak var observedScrollView: UIScrollView? private var observation: NSKeyValueObservation? override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear isUserInteractionEnabled = false } required init?(coder: NSCoder) { super.init(coder: coder) } deinit { observation?.invalidate() } override func didMoveToSuperview() { super.didMoveToSuperview() attachIfNeeded() } override func didMoveToWindow() { super.didMoveToWindow() attachIfNeeded() } func attachIfNeeded() { guard let scrollView = findEnclosingScrollView() else { return } guard scrollView !== observedScrollView else { return } observation?.invalidate() observedScrollView = scrollView observation = scrollView.observe(\.contentOffset, options: [.initial, .new]) { [weak self] sv, _ in self?.onOffsetChange(sv.contentOffset.y) } } private func findEnclosingScrollView() -> UIScrollView? { var view: UIView? = self while let current = view { if let scrollView = current as? UIScrollView { return scrollView } view = current.superview } return nil } } #endif