可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
So I have a ScrollView
holding a set of views:
ScrollView {
ForEach(cities) { city in
NavigationLink(destination: ...) {
CityRow(city: city)
}
.buttonStyle(BackgroundButtonStyle())
}
}
In every view I have a drag gesture:
let drag = DragGesture()
.updating($gestureState) { value, gestureState, _ in
// ...
}
.onEnded { value in
// ...
}
Which I assign to a part of the view:
ZStack(alignment: .leading) {
HStack {
// ...
}
HStack {
// ...
}
.gesture(drag)
}
As soon as I attach the gesture, the ScrollView
stop scrolling. The only way to make it scroll it to start scrolling from a part of it which has no gesture attached. How can I avoid it and make both work together. In UIKit is was as simple as specifying true
in shouldRecognizeSimultaneouslyWith
method. How can I have the same in SwiftUI?
In SwiftUI I've tried attaching a gesture using .simultaneousGesture(drag)
and .highPriorityGesture(drag)
– they all work the same as .gesture(drag)
. I've also tried providing all possible static GestureMask
values for including:
parameter – I have either scroll working or my drag gesture working. Never both of them.
Here's what I'm using drag gesture for:
回答1:
I have created an easy to use extension based on the Michel's answer.
struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
}
}
extension View {
func delayTouches() -> some View {
Button(action: {}) {
highPriorityGesture(TapGesture())
}
.buttonStyle(NoButtonStyle())
}
}
You apply it after using a drag gesture.
Example:
ScrollView {
YourView()
.gesture(DragGesture(minimumDistance: 0)
.onChanged { _ in }
.onEnded { _ in }
)
.delayTouches()
}
回答2:
You can set minimumDistance to some value (for instance 30).
Then the drag only works when you drag horizontally and reach the minimum distance, otherwise the scrollview or list gesture override the view gesture
.gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local)
回答3:
I believe what you want is
simultaneousGesture(_:including:)
, which is documented here.
HStack {
// ...
}
.simultaneousGesture(drag)
回答4:
I finally found a solution that seems to work with me. I have found Button
to be magical creatures. They propagate events properly, and keep on working even if you are inside a ScrollView
or a List
.
Now, you will say
Yeah, but Michel, I don't want a friggin button that taps with some effects, I want to long-press something, or drag something.
Fair enough. But you must consider the Button
of lore as something that actually makes everything underneath its label:
as actually working correctly, if you know how to do things! Because the Button will actually try to behave, and delegate its gestures to controls underneath if they actually implement onTapGesture
, so you can get a toggle or an info.circle
button you can tap inside. In other words, All gestures that appears after the onTapGesture {}
(but not the ones before) will work.
As a complex code example, what you must have is as follow:
ScrollView {
Button(action: {}) { // Makes everything behave in the "label:"
content // Notice this uses the ViewModifier ways ... hint hint
.onTapGesture {} // This view overrides the Button
.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture(coordinateSpace: .global))
.updating(self.$dragState) { ...
The example uses a complex gesture because I wanted to show they do work, as long as that elusive Button
/onTapGesture
combo are there.
Now you will notice this is not totally perfect, the long press is actually long-pressed too by the button before it delegates its long press to yours (so that example will have more than 0.01 second of long press). Also, you must have a ButtonStyle
if you wish to remove the pressed effects. In other words, YMMV, a lot of testing, but for my own usage, this is the closest I've been able to make an actual long press / drag work in a List of items.
回答5:
I attempted to implement a similar list style in my app only to find that the gestures conflicted with the ScrollView. After having spent hours researching and attempting possible fixes and workarounds for this issue, as of XCode 11.3.1
, I believe this to be a bug that Apple needs to resolve in future versions of SwiftUI.
A Github repo with sample code to replicate the issue has been put together here and has been reported to Apple with the reference FB7518403
.
Here's hoping it is fixed soon!
回答6:
Just before
.gesture(drag)
You can add
.onTapGesture { }
This works for me, apparently adding a tapGesture avoids confusion between the two DragGestures.
I hope this helps
回答7:
I had a similar problem with dragging a slider at:
stackoverflow question
This is the working answer code, with the "trick" of the "DispatchQueue.main.asyncAfter"
Maybe you could try something similar for your ScrollView.
struct ContentView: View {
@State var pos = CGSize.zero
@State var prev = CGSize.zero
@State var value = 0.0
@State var flag = true
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.prev.width, height: value.translation.height + self.prev.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.prev.width, height: value.translation.height + self.prev.height)
self.prev = self.pos
}
return VStack {
Slider(value: $value, in: 0...100, step: 1) { _ in
self.flag = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.flag = true
}
}
}
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.gesture(flag ? drag : nil)
}
}
回答8:
I can't find a pure SwiftUI solution to this so I used a UIViewRepresentable as a work around. In the meantime, I've submitted a bug to Apple. Basically, I've created a clear view with a pan gesture on it which I will present over any SwiftUI view I want to add the gesture to. It's not a perfect solution, but maybe it's good enough for you.
public struct ClearDragGestureView: UIViewRepresentable {
public let onChanged: (ClearDragGestureView.Value) -> Void
public let onEnded: (ClearDragGestureView.Value) -> Void
/// This API is meant to mirror DragGesture,.Value as that has no accessible initializers
public struct Value {
/// The time associated with the current event.
public let time: Date
/// The location of the current event.
public let location: CGPoint
/// The location of the first event.
public let startLocation: CGPoint
public let velocity: CGPoint
/// The total translation from the first event to the current
/// event. Equivalent to `location.{x,y} -
/// startLocation.{x,y}`.
public var translation: CGSize {
return CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y)
}
/// A prediction of where the final location would be if
/// dragging stopped now, based on the current drag velocity.
public var predictedEndLocation: CGPoint {
let endTranslation = predictedEndTranslation
return CGPoint(x: location.x + endTranslation.width, y: location.y + endTranslation.height)
}
public var predictedEndTranslation: CGSize {
return CGSize(width: estimatedTranslation(fromVelocity: velocity.x), height: estimatedTranslation(fromVelocity: velocity.y))
}
private func estimatedTranslation(fromVelocity velocity: CGFloat) -> CGFloat {
// This is a guess. I couldn't find any documentation anywhere on what this should be
let acceleration: CGFloat = 500
let timeToStop = velocity / acceleration
return velocity * timeToStop / 2
}
}
public class Coordinator: NSObject, UIGestureRecognizerDelegate {
let onChanged: (ClearDragGestureView.Value) -> Void
let onEnded: (ClearDragGestureView.Value) -> Void
private var startLocation = CGPoint.zero
init(onChanged: @escaping (ClearDragGestureView.Value) -> Void, onEnded: @escaping (ClearDragGestureView.Value) -> Void) {
self.onChanged = onChanged
self.onEnded = onEnded
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc func gestureRecognizerPanned(_ gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else {
Log.assertFailure("Missing view on gesture")
return
}
switch gesture.state {
case .possible, .cancelled, .failed:
break
case .began:
startLocation = gesture.location(in: view)
case .changed:
let value = ClearDragGestureView.Value(time: Date(),
location: gesture.location(in: view),
startLocation: startLocation,
velocity: gesture.velocity(in: view))
onChanged(value)
case .ended:
let value = ClearDragGestureView.Value(time: Date(),
location: gesture.location(in: view),
startLocation: startLocation,
velocity: gesture.velocity(in: view))
onEnded(value)
@unknown default:
break
}
}
}
public func makeCoordinator() -> ClearDragGestureView.Coordinator {
return Coordinator(onChanged: onChanged, onEnded: onEnded)
}
public func makeUIView(context: UIViewRepresentableContext<ClearDragGestureView>) -> UIView {
let view = UIView()
view.backgroundColor = .clear
let drag = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.gestureRecognizerPanned))
drag.delegate = context.coordinator
view.addGestureRecognizer(drag)
return view
}
public func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<ClearDragGestureView>) {
}
}