如何在SwiftUI中使LongPressGesture和ScrollView同时工作?

huangapple go评论70阅读模式
英文:

How to make LongPressGesture and scrolling in ScrollView work together at the same time in SwiftUI?

问题

以下是您要翻译的代码部分:

import SwiftUI

struct TextBox: View {
  var text: String
  var color: Color
  @GestureState private var isLongPressure: Bool = false
  
  var body: some View {
    let longTap = LongPressGesture(minimumDuration: 0.3)
      .updating($isLongPressure) { state, newState, transaction in
        newState = state
        transaction.animation = .easeOut(duration: 0.2)
      }
    
    Text(text)
      .frame(width: 400, height: 200)
      .background(isLongPressure ? .white : color)
      .simultaneousGesture(longTap)
  }
}

struct TestGestures: View {
    var body: some View {
      ScrollView {
        TextBox(text: "Test 1", color: .red)
        TextBox(text: "Test 2", color: .green)
        TextBox(text: "Test 3", color: .blue)
        TextBox(text: "Test 4", color: .red)
        TextBox(text: "Test 5", color: .green)
        TextBox(text: "Test 6", color: .blue)
      }
    }
}

struct TestGestures_Previews: PreviewProvider {
    static var previews: some View {
        TestGestures()
    }
}

希望这对您有所帮助。

英文:

Let's imagine, here is a ScrollView with some elements and I want to make some actions (e.g. changing of color) on long tap on these elements. But also I want to make possible to scroll this view.

Here is an example:

如何在SwiftUI中使LongPressGesture和ScrollView同时工作?

import SwiftUI

struct TextBox: View {
  var text: String
  var color: Color
  @GestureState private var isLongPressure: Bool = false
  
  var body: some View {
    let longTap = LongPressGesture(minimumDuration: 0.3)
      .updating($isLongPressure) { state, newState, transaction in
        newState = state
        transaction.animation = .easeOut(duration: 0.2)
      }
    
    Text(text)
      .frame(width: 400, height: 200)
      .background(isLongPressure ? .white : color)
      .simultaneousGesture(longTap)
  }
}

struct TestGestures: View {
    var body: some View {
      ScrollView {
        TextBox(text: "Test 1", color: .red)
        TextBox(text: "Test 2", color: .green)
        TextBox(text: "Test 3", color: .blue)
        TextBox(text: "Test 4", color: .red)
        TextBox(text: "Test 5", color: .green)
        TextBox(text: "Test 6", color: .blue)
      }
    }
}

struct TestGestures_Previews: PreviewProvider {
    static var previews: some View {
        TestGestures()
    }
}

So, if I comment .simultaneousGesture(longTap) – scrolling works, but if I uncomment it – scrolling stopped work.

P.S.: I've tried to add onTapGesture before adding longTap and it doesn't help.

Thanks in advance!


Update:

Thanks for the solution by @nickreps:

如何在SwiftUI中使LongPressGesture和ScrollView同时工作?

import SwiftUI

struct TextBox: View {
  var text: String
  var color: Color
  @GestureState private var isLongPressure: Bool = false
  
  var body: some View {
    let longTap = LongPressGesture(minimumDuration: 0.3)
      .updating($isLongPressure) { value, state, transaction in
        state = value
        transaction.animation = .easeOut(duration: 0.2)
      }
    
    Text(text)
      .frame(width: 400, height: 200)
      .background(isLongPressure ? .white : color)
      .delaysTouches(for: 0.01) {
        //some code here, if needed
      }
      .gesture(longTap)
  }
}

struct TestGestures: View {
    var body: some View {
      ScrollView {
        TextBox(text: "Test 1", color: .red)
        TextBox(text: "Test 2", color: .green)
        TextBox(text: "Test 3", color: .blue)
        TextBox(text: "Test 4", color: .red)
        TextBox(text: "Test 5", color: .green)
        TextBox(text: "Test 6", color: .blue)
      }
    }
}

extension View {
    func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void = {}) -> some View {
        modifier(DelaysTouches(duration: duration, action: action))
    }
}

fileprivate struct DelaysTouches: ViewModifier {
    @State private var disabled = false
    @State private var touchDownDate: Date? = nil
    
    var duration: TimeInterval
    var action: () -> Void
    
    func body(content: Content) -> some View {
        Button(action: action) {
            content
        }
        .buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
        .disabled(disabled)
    }
}

fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
    @Binding var disabled: Bool
    var duration: TimeInterval
    @Binding var touchDownDate: Date?
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed, perform: handleIsPressed)
    }
    
    private func handleIsPressed(isPressed: Bool) {
        if isPressed {
            let date = Date()
            touchDownDate = date
            
            DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
                if date == touchDownDate {
                    disabled = true
                    
                    DispatchQueue.main.async {
                        disabled = false
                    }
                }
            }
        } else {
            touchDownDate = nil
            disabled = false
        }
    }
}


struct TestGestures_Previews: PreviewProvider {
    static var previews: some View {
        TestGestures()
    }
}

答案1

得分: 2

我能够通过使用按钮而不是 TextView 来使其工作。虽然这直接使用了您提供的代码,但您应该能够修改一些部分以使其满足您的需求(如果需要,我可以帮忙!)

import SwiftUI

struct ScrollTest: View {
    let testData = [1]
    
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            AnimatedButtonView(color: .red, text: "测试1")
            AnimatedButtonView(color: .green, text: "测试2")
            AnimatedButtonView(color: .blue, text: "测试3")
        }
    }
}



struct AnimatedButtonView: View {
    @GestureState var isDetectingLongPress = false
    let color: Color
    let text: String
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 12.5, style: .continuous)
                .fill(color)
                .frame(width: UIScreen.main.bounds.width, height: 200)
                .padding(25)
                .scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
                .brightness(!isDetectingLongPress ? 0.0 : -0.125)
                .animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
            Text(text)
        }
        
        .delaysTouches(for: 0.01) {
            //some code here, if needed
        }
        .gesture(
            LongPressGesture(minimumDuration: 3)
                .updating($isDetectingLongPress) { currentState, gestureState,
                    transaction in
                    gestureState = currentState
                    transaction.animation = Animation.easeIn(duration: 2.0)
                }
                .onEnded { finished in
                    print("手势结束")
                })
        
    }
}

extension View {
    func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void) -> some View {
        modifier(DelaysTouches(duration: duration, action: action))
    }
}

fileprivate struct DelaysTouches: ViewModifier {
    @State private var disabled = false
    @State private var touchDownDate: Date? = nil
    
    var duration: TimeInterval
    var action: () -> Void
    
    func body(content: Content) -> some View {
        Button(action: action) {
            content
        }
        .buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
        .disabled(disabled)
    }
}

fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
    @Binding var disabled: Bool
    var duration: TimeInterval
    @Binding var touchDownDate: Date?
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed, perform: handleIsPressed)
    }
    
    private func handleIsPressed(isPressed: Bool) {
        if isPressed {
            let date = Date()
            touchDownDate = date
            
            DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
                if date == touchDownDate {
                    disabled = true
                    
                    DispatchQueue.main.async {
                        disabled = false
                    }
                }
            }
        } else {
            touchDownDate = nil
            disabled = false
        }
    }
}
英文:

I was able to get it working by utilizing a button rather than a TextView. Although this does directly utilize the code you provided, you should be able to modify some pieces to have it meet your needs (I can help with this, if needed!)

import SwiftUI
struct ScrollTest: View {
let testData = [1]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
AnimatedButtonView(color: .red, text: "Test 1")
AnimatedButtonView(color: .green, text: "Test 2")
AnimatedButtonView(color: .blue, text: "Test 3")
}
}
}
struct AnimatedButtonView: View {
@GestureState var isDetectingLongPress = false
let color: Color
let text: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(color)
.frame(width: UIScreen.main.bounds.width, height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
Text(text)
}
.delaysTouches(for: 0.01) {
//some code here, if needed
}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("gesture ended")
})
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
@State private var disabled = false
@State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
@Binding var disabled: Bool
var duration: TimeInterval
@Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}

答案2

得分: 0

我不确定我理解确切的上下文,但你可以添加一个条件,以便你的 LongPressGesture 仅在手势未用于滚动时触发操作。

let longTap = LongPressGesture(minimumDuration: 0.3)
  .updating($isLongPressure) { value, state, transaction in
    if value {
      state = true
      transaction.animation = .easeOut(duration: 0.2)
    }
  }
英文:

I'm not sure I understand the exact context, but you could add a condition so your LongPressGesture only triggers an action when gesture is not being used for scrolling.

let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { value, state, transaction in
if value {
state = true
transaction.animation = .easeOut(duration: 0.2)
}
}

答案3

得分: 0

这是@nickreps解决方案的一个版本,但稍微更短一些:

struct MyView: View {
    
    var body: some View {
        ScrollView { VStack { ForEach(0..<40) { _ in Item() } } }
    }
    
}

struct Item: View {
    
    var body: some View {
        Color.yellow
            .frame(height: 100)
            .border(Color.black, width: 1)
            .yieldTouches() // <- 这解决了手势冲突
            .gesture(
                LongPressGesture(minimumDuration: 1, maximumDistance: 10)
                    .onEnded { _ in print("你好!") }
             )
    }
    
}

public extension View {
    
    func yieldTouches() -> some View { modifier(YieldTouches()) }
    
}

private struct YieldTouches: ViewModifier {
    
    @State private var disabled = false
    
    func body(content: Content) -> some View {
        content
            .disabled(disabled)
            .onTapGesture { onMain { disabled = true; onMain { disabled = false } } }
    }
    
    private func onMain(_ action: @escaping () -> Void) { DispatchQueue.main.async(execute: action) }
    
}
英文:

Here is a version of @nickreps solution, but a bit shorter:

struct MyView : View {
var body: some View {
ScrollView { VStack { ForEach(0..&lt;40) { _ in Item() } } }
}
}
struct Item : View {
var body: some View {
Color.yellow
.frame(height: 100)
.border(Color.black, width: 1)
.yieldTouches() // &lt;- this solves gestures conflict
.gesture(
LongPressGesture(minimumDuration: 1, maximumDistance: 10)
.onEnded { _ in print(&quot;hello!&quot;) }
)
}
}
public extension View {
func yieldTouches() -&gt; some View { modifier(YieldTouches()) }
}
private struct YieldTouches : ViewModifier {
@State private var disabled = false
func body(content: Content) -&gt; some View {
content
.disabled(disabled)
.onTapGesture { onMain { disabled = true; onMain { disabled = false } } }
}
private func onMain(_ action: @escaping () -&gt; Void) { DispatchQueue.main.async(execute: action) }
}

huangapple
  • 本文由 发表于 2023年2月18日 22:52:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/75494141.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定