1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import SwiftUI
import UIKit
/// Disables the nearest ancestor `UIScrollView`'s pan gesture recognizer when
/// `isDisabled` is true. Unlike SwiftUI's `.scrollDisabled`, this only blocks
/// USER-driven panning — programmatic `setContentOffset` / `ScrollViewProxy.scrollTo`
/// continues to work, which is essential for the strip's auto-scroll during a
/// reorder drag.
///
/// Attach as `.background { ScrollPanLocker(isDisabled: isReordering) }` on the
/// SwiftUI view that lives inside the scroll view whose pan you want to lock.
struct ScrollPanLocker: UIViewRepresentable {
let isDisabled: Bool
func makeUIView(context _: Context) -> RecognizerFinderView {
let view = RecognizerFinderView(frame: .zero)
view.isUserInteractionEnabled = false
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: RecognizerFinderView, context _: Context) {
guard let scrollView = uiView.enclosingScrollView() else { return }
// Toggling isEnabled on the pan recognizer immediately cancels any
// in-flight drag and blocks new user pans, but leaves programmatic
// setContentOffset untouched.
scrollView.panGestureRecognizer.isEnabled = !isDisabled
}
final class RecognizerFinderView: UIView {
func enclosingScrollView() -> UIScrollView? {
var responder: UIResponder? = self
while let next = responder?.next {
if let scrollView = next as? UIScrollView {
return scrollView
}
responder = next
}
return nil
}
}
}
/// Disables the enclosing `UINavigationController`'s interactive-pop gesture
/// recognizer when `isDisabled` is true. Use to prevent iOS's edge-swipe-to-pop
/// from firing while the user is in a reorder drag.
///
/// Attach as `.background { InteractivePopLocker(isDisabled: isReordering) }`
/// on the root view of the pushed view controller.
struct InteractivePopLocker: UIViewRepresentable {
let isDisabled: Bool
func makeUIView(context _: Context) -> ResponderFinderView {
let view = ResponderFinderView(frame: .zero)
view.isUserInteractionEnabled = false
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: ResponderFinderView, context _: Context) {
guard let nav = uiView.enclosingNavigationController() else { return }
nav.interactivePopGestureRecognizer?.isEnabled = !isDisabled
}
final class ResponderFinderView: UIView {
func enclosingNavigationController() -> UINavigationController? {
var responder: UIResponder? = self
while let next = responder?.next {
if let vc = next as? UIViewController {
return vc.navigationController
}
responder = next
}
return nil
}
}
}