如何在SwiftUI中检测并采取操作当按钮被按下时

4

我想在按钮按下开始时执行一个动作,然后在按钮停止被按下时执行另一个动作。我一开始寻找的是一个简单的解决方案,但却陷入了更复杂的配置中。从BlueSpud得到的一个相当简单且接近的选项是:不使用按钮动作,而是尝试:

    struct MyView: View {
    @State private var pressing = false

    var body: some View {

        Text("Button")
            .background(self.pressing ? Color.red : Color.blue)
            .gesture(DragGesture(minimumDistance: 0.0)
                .onChanged { _ in
                    self.pressing = true
                    print("Pressing started and/or ongoing")
            }
            .onEnded { _ in
                self.pressing = false
                print("Pressing ended")
            })
    }
}

这段代码的问题在于,如果您在按下按钮时将手指拖出按钮区域,则.onEnded永远不会被调用,而且由于事件没有可靠的结束,解决方案无法正常工作。
我还尝试了苹果关于组合SwiftUI手势的示例。它提供了非常一致的控制压下和未压下状态,但我似乎不知道在哪里插入我的操作:
struct PressedButton: View {

    var startAction: ()->Void
    var endAction: ()->Void

    enum DragState {
        case inactive
        case pressing
        case dragging(translation: CGSize)

        var translation: CGSize {
            switch self {
            case .inactive, .pressing:
                return .zero
            case .dragging(let translation):
                return translation
            }
        }

        var isActive: Bool {
            switch self {
            case .inactive:
                print("DragState inactive but I can't add my action here")
                //self.endAction()
                return false
            case .pressing, .dragging:
                return true
            }
        }

        var isDragging: Bool {
            switch self {
            case .inactive, .pressing:
                return false
            case .dragging:
                return true
            }
        }
    }

    @GestureState var dragState = DragState.inactive

    var body: some View {
        let longPressDrag = LongPressGesture(minimumDuration: 0.1)
        .sequenced(before: DragGesture())
        .updating($dragState) { value, state, transaction in
            switch value {

            // Long press begins.
            case .first(true):
                print("Long press begins. I can add my action here")
                self.startAction()
                state = .pressing

            // Long press confirmed, dragging may begin.
            case .second(true, let drag):
                //print("Long press dragging")
                state = .dragging(translation: drag?.translation ?? .zero)

             // Dragging ended or the long press cancelled.
            default:
                print("Long press inactive but it doesn't get called")
                state = .inactive
            }
        }
        .onEnded { _ in
            print("Long press ended but it doesn't get called")
            }

        return Text("Button")
            .background(dragState.isActive ? Color.purple : Color.orange)
            .gesture(longPressDrag)
    }
}

请看我在另一个问题这里提供的解决方案。看起来正是你需要的。 - George
2个回答

7

不需要使用UIView和Representable,可以像这样使用内置的SwiftUI isPressed属性来配置Button:

import SwiftUI

struct MyButtonStyle: ButtonStyle
{
    func makeBody(configuration: Configuration) -> some View
    {
        if(configuration.isPressed)
        {
            // call your action here but don't change @State of current view
            print("Button is pressed")
        }
        else
        {
            // call your stop-action here but don't change @State of current view
            print("Button released")
        }
        
        return configuration.label
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(20)
            .scaleEffect(configuration.isPressed ? 0.8 : 1)
            .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

struct ButtonTestView: View
{
    var body: some View
    {
        Button("Press me")
        {
            print("Button action")
        }
        .buttonStyle(MyButtonStyle())
    }
}

@main
struct TestApp: App
{
    var body: some Scene
    {
        WindowGroup
        {
            ButtonTestView()
        }
    }
}


我不确定这个是否有效。在Swift 5中,MyButtonStyle()似乎会在按钮实际被按下之前制造出按下和未按下的样式,导致两个打印语句都会触发。试图在此时修改任何状态——在视图制造期间——是非法的,因此我认为你在这里无法做任何有用的事情。 - Nicholas Jackiw
不确定你所说的“非法”是什么意思,即使在Swift 5中,这是一个完全可用的示例。我没有看到任何证据表明在buttonStyle结构中开发UI行为是非法的。对于我来说,请注意这不是一个“State”,而是在View制造过程中的UI外观决策,使用此解决方案可以在应用商店上获得批准。因此,如果你能分享你所依赖的文件/政策中关于这是非法的链接,我会很感激。 - Shaybc
1
抱歉使用了“非法”这个词,我应该用“未定义”。具体来说,任何影响状态的代码放在“按钮被按下/释放”子句中会导致Xcode发出以下警告:“在视图更新期间修改状态;这将导致未定义的行为。”您可能是正确的,即使存在此问题,应用程序也可以获得批准,并且当然可以通过仅在对程序状态没有影响的按钮中使用此代码来消除此问题。对于我而言,我希望有一个更像Button()常规操作过程的解决方案,它发生在视图重新制造周期之外,因此允许状态修改。 - Nicholas Jackiw

2
由于原生的SwiftUI目前无法实现您想要的功能,我建议采用以下方法,这种方法是有效和可管理的,因此也是可靠的。
演示展示了基于使用UIGestureRecongnizer/UIViewRepresentable的简化代码,可以轻松扩展(例如,如果您想要拦截touchesCanceled、点击计数等)。
import SwiftUI
import UIKit

class MyTapGesture : UITapGestureRecognizer {

    var didBeginTouch: (()->Void)?
    var didEndTouch: (()->Void)?

    init(target: Any?, action: Selector?, didBeginTouch: (()->Void)? = nil, didEndTouch: (()->Void)? = nil) {
        super.init(target: target, action: action)
        self.didBeginTouch = didBeginTouch
        self.didEndTouch = didEndTouch
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        self.didBeginTouch?()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesEnded(touches, with: event)
        self.didEndTouch?()
    }
}

struct TouchesHandler: UIViewRepresentable {
    var didBeginTouch: (()->Void)?
    var didEndTouch: (()->Void)?

    func makeUIView(context: UIViewRepresentableContext<TouchesHandler>) -> UIView {
        let view = UIView(frame: .zero)
        view.isUserInteractionEnabled = true
        view.addGestureRecognizer(context.coordinator.makeGesture(didBegin: didBeginTouch, didEnd: didEndTouch))
        return view;
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<TouchesHandler>) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }

    class Coordinator {
        @objc
        func action(_ sender: Any?) {
            print("Tapped!")
        }

        func makeGesture(didBegin: (()->Void)?, didEnd: (()->Void)?) -> MyTapGesture {
            MyTapGesture(target: self, action: #selector(self.action(_:)), didBeginTouch: didBegin, didEndTouch: didEnd)
        }
    }
    typealias UIViewType = UIView
}

struct TestCustomTapGesture: View {
    var body: some View {
        Text("Hello, World!")
            .padding()
            .background(Color.yellow)
            .overlay(TouchesHandler(didBeginTouch: {
                print(">> did begin")
            }, didEndTouch: {
                print("<< did end")
            }))
    }
}

struct TestCustomTapGesture_Previews: PreviewProvider {
    static var previews: some View {
        TestCustomTapGesture()
    }
}

对我来说,长按(类似按住说话)的功能无法正常工作。 我只需要做一个简单的更改,使用UILongPressGestureRecognizer代替UITapGestureRecognizer - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接