当用户在 SwiftUI 中停止长按手势时如何触发事件处理程序?

iSp*_*n17 3 ios swift swiftui

根据文档,.OnEnded事件处理程序将LongPressGesture在成功检测到时触发。当用户在检测到手势后停止按下时,如何触发事件?

下面是一个例子:

  • 用户按下例如 2 秒。

  • ** 有东西出现 **

  • 用户再过 2 秒后释放

  • ** 东西消失了 **

小智 11

似乎这里可能有一个更优雅的解决方案,尽管它并不立即显现出来。

您可以使用最小距离为 0 的 DragGesture,而不是使用 LongPressGesture

@State var pressing = false
Text("Press Me").gesture(
    DragGesture(minimumDistance: 0)
        .onChanged({ _ in
            pressing = true
        })
        .onEnded({ _ in
            doThingWhenReleased()
        })
)
Run Code Online (Sandbox Code Playgroud)

在此示例中,按下时按下将为 true,松开时将变为 false。当项目被释放时, onEnded 闭包将被调用。以下是将这个功能捆绑到 ViewModifier 中使用,但如果您希望它具有与其他手势类似的 api,那么作为独立的 Gesture() 可能会更好:

struct PressAndReleaseModifier: ViewModifier {
    @Binding var pressing: Bool
    var onRelease: () -> Void

    func body(content: Content) -> some View {
        content
            .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onChanged{ state in
                        
                        pressing = true
                    }
                    .onEnded{ _ in
                        pressing = false
                        onRelease()
                    }
            )
    }
}

extension View {
    func pressAndReleaseAction(pressing: Binding<Bool>, onRelease: @escaping (() -> Void)) -> some View {
        modifier(PressAndReleaseModifier(pressing: pressing, onRelease: onRelease))
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它

struct SomeView: View {
    @State var pressing
    var body: some View {
        Text(pressing ? "Press Me!" : "Pressed")
            .pressAndReleaseAction(pressing: $pressing, onRelease: {
                print("Released")
            })
    }
}
Run Code Online (Sandbox Code Playgroud)

唯一的缺点是,如果用户将鼠标拖动到按钮之外并释放,onRelease() 仍然会被触发。这可能会在 ViewModifier 内部更新以获得预期结果,用户可以将其拖动到外部以取消操作,但您可能必须使用 GeometryReader。


iSp*_*n17 10

我设法解决了它,但如果有人有更简单的解决方案,我会很乐意接受。

基本上我需要将 2 LongPressGesture-s 链接在一起。

第一个将在长按 2 秒后生效 - 这是something应该出现的时间。

第二个会在Double.infinity一段时间后生效,这意味着它永远不会完成,所以用户可以按他们想要的时间。对于这种效果,我们只关心取消时的事件——这意味着用户停止按下。

@GestureState private var isPressingDown: Bool = false

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //This means the first Gesture completed
                state = true //Update the GestureState
            default: break
        }
    })
    .
[...]

something.opacity(isPressingDown ? 1 : 0)
Run Code Online (Sandbox Code Playgroud)

LongPressGesture通过调用该.sequenced(before:)方法对两个-s 进行排序时,您会得到一个

SequenceGesture<LongPressGesture, LongPressGesture> 作为返回值

它的枚举中有一个.first(Bool)和一个.second(Bool, Bool?)案例Value

这种.first(Bool)情况是第一个 LongPressGesture还没有结束的时候。

这种.second(Bool, Bool?)情况是第一次 LongPressGesture结束时。

因此,当SequenceGesture的值为 时.second(true, nil),这意味着第一个 Gesture 已完成而第二个尚未定义 - 这是应该显示某些内容的时候- 这就是我们将state变量设置true为该 case 内部的原因(该state变量封装了isPressingDown变量,因为它被作为.updating(_:body:)方法的第一个参数)

并且我们不需要做任何关于设置state返回的事情,false因为当使用该.updating(_:body:)方法时,状态会返回到它的初始值——也就是false——如果用户取消手势。这将导致“某物”的消失。(这里取消意味着我们在手势结束所需的最短秒数之前抬起手指——第二个手势是无限秒。)

因此,根据文档的部分,请务必注意,在取消手势时不会调用.updating(_:body:)方法的回调Update Transient UI State


2021 年 3 月 24 日编辑:

在我看来,我遇到了更新 a@Published属性的问题@ObservedObject。由于.updating()在重置时不会调用方法闭包,因此GestureState您需要另一种方法来重置@Published属性。解决该问题的方法是添加另一个名为的视图修饰符.onChange(of:perform:)

模型.swift:

class Model: ObservableObject {
    @Published isPressedDown: Bool = false
    
    private var cancellableSet = Set<AnyCancellable>()

    init() {
        //Do Something with isPressedDown
        $isPressedDown
            .sink { ... }
            .store(in: &cancellableSet)
    }
}
Run Code Online (Sandbox Code Playgroud)

查看.swift:

@GestureState private var _isPressingDown: Bool = false
@ObservedObject var model: Model

[...]

aView.gesture(LongPressGesture(minimumDuration: 2.0)
    .sequenced(before: LongPressGesture(minimumDuration: .infinity))
    .updating($_isPressingDown) { value, state, transaction in
        switch value {            
            case .second(true, nil): //This means the first Gesture completed
                state = true //Update the GestureState
                model.isPressedDown = true //Update the @ObservedObject property
            default: break
        }
    })
    .onChange(of: _isPressingDown) { value in
        if !value {
            model.isPressedDown = false //Reset the @ObservedObject property
        }
    })
Run Code Online (Sandbox Code Playgroud)