mirror of
https://github.com/yattee/yattee.git
synced 2026-10-26 06:32:53 +00:00
Attach the overscroll collapse gesture to the geometrically matching scroll view
The handler picked the first scroll view under the nearest ancestor, which in the corner layout is one of the side column pages, so pulling down on the expanded below-video panel never collapsed it. Match the background view's window frame instead and re-attach when the match changes.
This commit is contained in:
@@ -38,9 +38,7 @@ struct OverscrollGestureView: UIViewRepresentable {
|
|||||||
|
|
||||||
// Schedule scroll view discovery after view is in hierarchy
|
// Schedule scroll view discovery after view is in hierarchy
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let scrollView = Self.findScrollView(from: view) {
|
Self.attachIfNeeded(from: view, coordinator: context.coordinator)
|
||||||
context.coordinator.gestureHandler.attach(to: scrollView)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -56,14 +54,35 @@ struct OverscrollGestureView: UIViewRepresentable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not attached yet, try again
|
// Attach if not yet attached, or re-attach when the geometric match
|
||||||
if context.coordinator.gestureHandler.scrollView == nil {
|
// changed (the hierarchy is flat under the SwiftUI host, so the
|
||||||
DispatchQueue.main.async {
|
// matching scroll view can differ from the one first found)
|
||||||
if let scrollView = Self.findScrollView(from: uiView) {
|
DispatchQueue.main.async {
|
||||||
context.coordinator.gestureHandler.attach(to: scrollView)
|
Self.attachIfNeeded(from: uiView, coordinator: context.coordinator)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func attachIfNeeded(from view: UIView, coordinator: Coordinator) {
|
||||||
|
guard view.window != nil else { return }
|
||||||
|
// Cheap check first: keep the current scroll view while it still
|
||||||
|
// matches our frame (updateUIView runs on every SwiftUI update)
|
||||||
|
if let attached = coordinator.gestureHandler.scrollView, attached.window != nil {
|
||||||
|
let hostFrame = view.convert(view.bounds, to: nil)
|
||||||
|
let frame = attached.convert(attached.bounds, to: nil)
|
||||||
|
if abs(frame.minX - hostFrame.minX) < 1, abs(frame.minY - hostFrame.minY) < 1,
|
||||||
|
abs(frame.width - hostFrame.width) < 1 {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
guard let scrollView = findScrollView(from: view) else { return }
|
||||||
|
if coordinator.gestureHandler.scrollView !== scrollView {
|
||||||
|
LoggingService.shared.debug(
|
||||||
|
"OverscrollGestureView attach",
|
||||||
|
category: .player,
|
||||||
|
details: "scrollView=\(Unmanaged.passUnretained(scrollView).toOpaque()) frame=\(scrollView.convert(scrollView.bounds, to: nil)) host=\(view.convert(view.bounds, to: nil))"
|
||||||
|
)
|
||||||
|
coordinator.gestureHandler.attach(to: scrollView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func dismantleUIView(_ uiView: UIView, coordinator: Coordinator) {
|
static func dismantleUIView(_ uiView: UIView, coordinator: Coordinator) {
|
||||||
@@ -72,10 +91,15 @@ struct OverscrollGestureView: UIViewRepresentable {
|
|||||||
|
|
||||||
// MARK: - Scroll View Discovery
|
// MARK: - Scroll View Discovery
|
||||||
|
|
||||||
/// Finds the UIScrollView in the view hierarchy.
|
/// Finds the UIScrollView this view is the `.background` of.
|
||||||
/// Since .background creates a separate branch, we need to find a common ancestor
|
/// `.background` creates a separate branch, and SwiftUI hosts platform
|
||||||
/// and search all its descendants.
|
/// views in a mostly flat hierarchy, so a common ancestor can hold many
|
||||||
|
/// unrelated scroll views (corner layout: side column pages + below-video
|
||||||
|
/// panel). A background view shares its frame with the view it backs, so
|
||||||
|
/// pick the scroll view whose window frame matches ours best.
|
||||||
private static func findScrollView(from view: UIView) -> UIScrollView? {
|
private static func findScrollView(from view: UIView) -> UIScrollView? {
|
||||||
|
let hostFrame = view.convert(view.bounds, to: nil)
|
||||||
|
|
||||||
// Collect all ancestors
|
// Collect all ancestors
|
||||||
var ancestors: [UIView] = []
|
var ancestors: [UIView] = []
|
||||||
var current: UIView? = view.superview
|
var current: UIView? = view.superview
|
||||||
@@ -86,16 +110,28 @@ struct OverscrollGestureView: UIViewRepresentable {
|
|||||||
|
|
||||||
// Search from each ancestor level, starting closest
|
// Search from each ancestor level, starting closest
|
||||||
for ancestor in ancestors {
|
for ancestor in ancestors {
|
||||||
// Collect all scroll views at this level
|
|
||||||
var scrollViews: [UIScrollView] = []
|
var scrollViews: [UIScrollView] = []
|
||||||
collectScrollViews(in: ancestor, into: &scrollViews)
|
collectScrollViews(in: ancestor, into: &scrollViews)
|
||||||
|
guard !scrollViews.isEmpty else { continue }
|
||||||
|
|
||||||
if !scrollViews.isEmpty {
|
// Best geometric match: smallest frame mismatch against our own
|
||||||
// Return the first one that has meaningful content (not a tiny internal scroll view)
|
// frame, among scroll views that actually overlap it. Ignore tiny
|
||||||
if let meaningful = scrollViews.first(where: { $0.frame.height > 100 }) {
|
// internal scroll views.
|
||||||
return meaningful
|
let candidates = scrollViews.filter { $0.frame.height > 100 }
|
||||||
}
|
let scored = (candidates.isEmpty ? scrollViews : candidates).compactMap { scrollView -> (UIScrollView, CGFloat)? in
|
||||||
return scrollViews.first
|
let frame = scrollView.convert(scrollView.bounds, to: nil)
|
||||||
|
guard hostFrame.isEmpty || frame.intersects(hostFrame) else { return nil }
|
||||||
|
let mismatch = abs(frame.minX - hostFrame.minX) + abs(frame.minY - hostFrame.minY)
|
||||||
|
+ abs(frame.width - hostFrame.width) + abs(frame.height - hostFrame.height)
|
||||||
|
return (scrollView, mismatch)
|
||||||
|
}
|
||||||
|
if let best = scored.min(by: { $0.1 < $1.1 }) {
|
||||||
|
return best.0
|
||||||
|
}
|
||||||
|
// Nothing overlaps at this level (e.g. not laid out yet) — keep
|
||||||
|
// climbing; fall back to the old behavior at the root.
|
||||||
|
if ancestor === ancestors.last {
|
||||||
|
return candidates.first ?? scrollViews.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user