I've made a minimum reproducible example that I'll paste below. Just plug it into Xcode and you'll see what the deal is. Essentially, I have a custom Slide-Up card. When it's enum position is .top, and I swipe the ScrollView in Xcode simulator, it causes the position of the card to slightly shift. Is there any way to lock the position of the card? Or at the very least make it so having the ScrollView inside of the slide-up card less problematic with swiping gestures?
Content View:
struct ContentView : View {
var body: some View {
ZStack(alignment: Alignment.top) {
Text("test")
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.red)
SlideOverCard {
VStack {
ScrollView {
VStack {
ForEach(1..<100) { _ in
Text("test")
}
}.frame(width: UIScreen.main.bounds.width)
}
Text("TESTER LINE OF TEXT")
Spacer()
}.frame(width: UIScreen.main.bounds.width)
}
}
.edgesIgnoringSafeArea(.vertical)
}
}
Slide-Up Card:
struct SlideOverCard<Content: View> : View {
@GestureState private var dragState = DragState.inactive
@State var position = CardPosition.top
var content: () -> Content
var body: some View {
let drag = DragGesture()
.updating($dragState) { drag, state, transaction in
state = .dragging(translation: drag.translation)
}
.onEnded(onDragEnded)
return Group {
Handle()
self.content()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.white)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.position.rawValue + self.dragState.translation.height)
.animation(self.dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.gesture(drag)
}
private func onDragEnded(drag: DragGesture.Value) {
let verticalDirection = drag.predictedEndLocation.y - drag.location.y
let cardTopEdgeLocation = self.position.rawValue + drag.translation.height
let positionAbove: CardPosition
let positionBelow: CardPosition
let closestPosition: CardPosition
if cardTopEdgeLocation <= CardPosition.middle.rawValue {
positionAbove = .top
positionBelow = .middle
} else {
positionAbove = .middle
positionBelow = .bottom
}
if (cardTopEdgeLocation - positionAbove.rawValue) < (positionBelow.rawValue - cardTopEdgeLocation) {
closestPosition = positionAbove
} else {
closestPosition = positionBelow
}
if verticalDirection > 0 {
self.position = positionBelow
} else if verticalDirection < 0 {
self.position = positionAbove
} else {
self.position = closestPosition
}
}
}
enum CardPosition: CGFloat {
case top = 100
case middle = 500
case bottom = 850
}
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
Handle:
struct Handle : View {
private let handleThickness = CGFloat(5.0)
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 40, height: handleThickness)
.foregroundColor(Color.secondary)
.padding(5)
}
}
Ok, I managed to reproduce what you meant...
Tested with Xcode 12 / iOS 14
Update: - here is found solution
ScrollView {
VStack {
ForEach(1..<100) { _ in
Text("test")
}
}.frame(width: UIScreen.main.bounds.width)
}
.background(Color.white) // << make opaque background
.highPriorityGesture(DragGesture()) // << block below DragGesture
Also I would consider variant to move drag gesture on "handle" (as you already have it)
return VStack { // << make it instead of Group
Handle()
.gesture(drag) // << here !!
self.content()
}
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.white)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.position.rawValue + self.dragState.translation.height)
.animation(self.dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))