i am trying to implement a custom gesture on this view that makes it so that only when a certain drag threshold is passed, does the underlying scrollview receive the drag touch events and begin scrolling all in one gesture. trying to mimic what i assume to be the gesture logic in the new ios 18 photos app
in my head i am envisioning it like the gesture blocks touch events from passing through until a certain threshold then it allows recognizing simultaneously with
enum DragState {
case dragging
case draggedUp
case draggedDown
case idle
}
struct Home: View {
// MARK: - Environment Variables
@Environment(.viewSize) private var viewSize
@Environment(.safeAreaInsets) private var safeAreaInsets
@EnvironmentObject private var windowState: UIState
// MARK: - State Variables
@State var isExpanded = false
@State var dragState: DragState = .idle
@State var scrollOffset: CGFloat = 0
@State var topScrollOffset: CGFloat = 0
@State var bottomOffset: CGFloat = 0
@State var topOffset: CGFloat = 0
// MARK: - Constants
let maxTranslation: CGFloat = 124
var body: some View {
let offset: CGFloat = viewSize.height * 0.4
ScrollView {
VStack(spacing: 0) {
ScrollView(.vertical) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 4)], spacing: 4) {
ForEach(1 ... 50, id: .self) { _ in
Rectangle()
.fill(Color(.systemGray5))
.frame(height: 120)
}
}
.offset(y: topOffset)
}
.onScrollGeometryChange(for: CGFloat.self, of: {
/// This will be zero when the content is placed at the bottom
$0.contentOffset.y - $0.contentSize.height + $0.containerSize.height
}, action: { oldValue, newValue in
guard oldValue != newValue else { return }
topScrollOffset = newValue
if isExpanded, newValue >= 0 {
//
}
})
.scrollClipDisabled()
.defaultScrollAnchor(.bottom)
.frame(height: viewSize.height)
.border(.blue, width: 2)
VStack(spacing: 4) {
ForEach(1 ... 12, id: .self) { _ in
Rectangle()
.fill(Color(.systemGray6))
.frame(height: 120)
}
}
.border(.red, width: 2)
.offset(y: bottomOffset)
}
}
.onScrollGeometryChange(for: CGFloat.self, of: {
/// This will be zero when the content is placed at the bottom
$0.contentOffset.y
}, action: { oldValue, newValue in
guard oldValue != newValue else { return }
scrollOffset = newValue
if !isExpanded, newValue <= 0 {
//
}
})
.defaultScrollAnchor(.top)
.scrollClipDisabled()
.frame(height: viewSize.height, alignment: .top)
.clipped()
.scaleEffect(0.65)
.gesture(
CustomGesture(isEnabled: true) { recognizer in
let translation = recognizer.translation(in: recognizer.view).y
let state = recognizer.state
// MARK: - Gesture Began
if state == .began {
dragState = .dragging
}
// MARK: - Gesture Changed
if state == .changed {
}
// MARK: - Gesture Ended
if state == .ended {
}
}
)
.onAppear {
DispatchQueue.main.async {
topOffset = -offset
bottomOffset = -offset
}
}
}
}
since SwiftUI gestures don’t work simultaneously with scrollView
struct CustomGesture: UIGestureRecognizerRepresentable {
var isEnabled: Bool
var handle: (UIPanGestureRecognizer) -> ()
func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator {
Coordinator()
}
func makeUIGestureRecognizer(context: Context) -> UIPanGestureRecognizer {
let gesture = UIPanGestureRecognizer()
gesture.delegate = context.coordinator
return gesture
}
func updateUIGestureRecognizer(_ recognizer: UIPanGestureRecognizer, context: Context) {
recognizer.isEnabled = isEnabled
}
func handleUIGestureRecognizerAction(_ recognizer: UIPanGestureRecognizer, context: Context) {
handle(recognizer)
}
class Coordinator: NSObject, UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
}
}