将 SwiftUI 与多个 UIGestureRecognizer 一起使用?

Posted

技术标签:

【中文标题】将 SwiftUI 与多个 UIGestureRecognizer 一起使用?【英文标题】:Using SwiftUI with multiple UIGestureRecognizers? 【发布时间】:2021-10-13 14:07:09 【问题描述】:

我目前正在尝试编写一个导航类似于 Affinity 或 Procreate 等应用的应用,您可以在其中使用单点触摸/拖动来绘制、润饰和交互,以及两指在画布上导航的手势。

我正在使用 SwiftUI 作为主要框架并根据需要包含 UIKit 来构建应用程序。

不幸的是,SwiftUI 还不允许像描述的那样复杂的手势,但 UIKit 通常可以。所以我恢复使用 UIKit 作为我的手势识别器,而不是依赖 SwiftUI 手势。然而,这导致了问题,即只会调用最顶层的手势识别器。 我希望同时识别多个手势 like demonstrated here,但不幸的是,SwiftUI 似乎会导致 UIViewRepresentables 出现问题。

有人可以帮我找出解决办法吗?

重要提示:我正在使用两个单独的视图来执行此操作,因为从长远来看,它们将用于不同的视图。但是,在示例中,出于演示目的,我将它们放在同一个视图中。

用法:

ZStack 
    DragGestureView  point in
        print("One Finger")
     dragEndedCallback: 
        print("One Finger Ended")
    

    TwoFingerNavigationView  point in
        viewStore.send(.dragChanged(point))
        print("Two Fingers")
     dragEndedCallback: 
        viewStore.send(.dragEnded)
        print("Two Fingers Ended")
     pinchedCallback:  value in
        viewStore.send(.magnificationChanged(value))
     pinchEndedCallback: 
        viewStore.send(.magnificationEnded)
    

    content()
        .position(viewStore.location)
        .scaleEffect(viewStore.scale * viewStore.offsetScale)

DragGestureView

public struct DragGestureView: UIViewRepresentable 
    let delegate = GestureRecognizerDelegate()
    var draggedCallback: ((CGPoint) -> Void)
    var dragEndedCallback: (() -> Void)

    public init(draggedCallback: @escaping ((CGPoint) -> Void), dragEndedCallback: @escaping (() -> Void)) 
        self.draggedCallback = draggedCallback
        self.dragEndedCallback = dragEndedCallback
    

    public class Coordinator: NSObject 
        var draggedCallback: ((CGPoint) -> Void)
        var dragEndedCallback: (() -> Void)

        public init(draggedCallback: @escaping ((CGPoint) -> Void),
             dragEndedCallback: @escaping (() -> Void)) 
            self.draggedCallback = draggedCallback
            self.dragEndedCallback = dragEndedCallback
        

        @objc func dragged(gesture: UIPanGestureRecognizer) 
            if gesture.state == .ended 
                self.dragEndedCallback()
             else 
                self.draggedCallback(gesture.location(in: gesture.view))
            
        
    

    class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate 
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool 
            return true
        
    

    public func makeUIView(context: UIViewRepresentableContext<DragGestureView>) -> DragGestureView.UIViewType 
        let view = UIView(frame: .zero)
        let gesture = UIPanGestureRecognizer(target: context.coordinator,
                                             action: #selector(Coordinator.dragged))
        gesture.minimumNumberOfTouches = 1
        gesture.maximumNumberOfTouches = 1
        gesture.delegate = delegate
        view.addGestureRecognizer(gesture)
        return view
    

    public func makeCoordinator() -> DragGestureView.Coordinator 
        return Coordinator(draggedCallback: self.draggedCallback,
                           dragEndedCallback: self.dragEndedCallback)
    

    public func updateUIView(_ uiView: UIView,
                      context: UIViewRepresentableContext<DragGestureView>) 
    

TwoFingerNavigationView

struct TwoFingerNavigationView: UIViewRepresentable 
    let delegate = GestureRecognizerDelegate()

    var draggedCallback: ((CGPoint) -> Void)
    var dragEndedCallback: (() -> Void)
    var pinchedCallback: ((CGFloat) -> Void)
    var pinchEndedCallback: (() -> Void)

    class Coordinator: NSObject 
        var draggedCallback: ((CGPoint) -> Void)
        var dragEndedCallback: (() -> Void)
        var pinchedCallback: ((CGFloat) -> Void)
        var pinchEndedCallback: (() -> Void)

        var startingDistance: CGFloat? = nil
        var isMagnifying = false
        var startingMagnification: CGFloat? = nil
        var newMagnification: CGFloat = 1.0

        init(draggedCallback: @escaping ((CGPoint) -> Void),
             dragEndedCallback: @escaping (() -> Void),
             pinchedCallback: @escaping ((CGFloat) -> Void),
             pinchEndedCallback: @escaping (() -> Void)) 
            self.draggedCallback = draggedCallback
            self.dragEndedCallback = dragEndedCallback
            self.pinchedCallback = pinchedCallback
            self.pinchEndedCallback = pinchEndedCallback
        

        @objc func dragged(gesture: UIPanGestureRecognizer) 
            if gesture.state == .ended 
                self.dragEndedCallback()
                self.pinchEndedCallback()
                startingDistance = nil
                isMagnifying = false
                startingMagnification = nil
                newMagnification = 1.0
             else 
                self.draggedCallback(gesture.translation(in: gesture.view) / (newMagnification))
            

            var touchLocations: [CGPoint] = []
            for i in 0..<gesture.numberOfTouches
                touchLocations.append(gesture.location(ofTouch: i, in: gesture.view))
            

            if touchLocations.count == 2 
                let distanceVector = (touchLocations[0] - touchLocations[1])
                let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)

                guard startingDistance != nil else  startingDistance = distance; return 
                guard distance - startingDistance! > 30 || distance - startingDistance! < -30 || isMagnifying else  return 
                isMagnifying = true;

                if startingMagnification == nil 
                    startingMagnification = distance / 100
                    pinchedCallback(1)
                 else 
                    let magnification = distance / 100
                    newMagnification = magnification / startingMagnification!
                    pinchedCallback(newMagnification)
                
            
        
    

    class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate 
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool 
            return true
        
    

    func makeUIView(context: UIViewRepresentableContext<TwoFingerNavigationView>) -> TwoFingerNavigationView.UIViewType 
        let view = UIView(frame: .zero)
        let gesture = UIPanGestureRecognizer(target: context.coordinator,
                                             action: #selector(Coordinator.dragged))
        gesture.minimumNumberOfTouches = 2
        gesture.maximumNumberOfTouches = 2
        gesture.delegate = delegate
        view.addGestureRecognizer(gesture)
        return view
    

    func makeCoordinator() -> TwoFingerNavigationView.Coordinator 
        return Coordinator(draggedCallback: self.draggedCallback,
                           dragEndedCallback: self.dragEndedCallback,
                           pinchedCallback: self.pinchedCallback,
                           pinchEndedCallback: self.pinchEndedCallback)
    

    func updateUIView(_ uiView: UIView,
                      context: UIViewRepresentableContext<TwoFingerNavigationView>) 
    

【问题讨论】:

你很幸运——你可以简单地将两个手势识别器放到一个实现 UIViewRepresentable 的类中。它适用于我的自定义手势识别器和标准 UITapGestureRecognizer。就我而言,我需要 SwiftUI 按钮才能与 UIKit 手势识别器一起使用,并且当我添加带有使用自定义 UIGestureRecognizer 的自定义视图的背景时,所有按钮都会停止工作(以及其他 SwiftUI 控件)。 【参考方案1】:

我没有在官方文档中找到任何关于我的发现的确认信息,但似乎 SwiftUI 手势的工作方式与 UIKit 不同。一般来说 - 对于复杂的情况,您可以使用一种或另一种。

如果你有 UIKit 元素,例如视图中的按钮,然后添加它们“接收所有触摸”的 SwiftUI 手势,并且 UIKit 元素停止工作(什么也不接收)。 如果您没有在使用 UIKit 元素的视图上添加任何 SwiftUI 手势,那么所有 UIKit 都可以正常工作。这就是自定义 UIKit 视图在 SwiftUI 中正常工作的原因。

如果您使用任何一种手势将 SwiftUI 视图逐个添加,那么,正如您所提到的,只有***视图手势有效。在 UIKit 中也是如此:1)首先 hitTests 找到给定点的所有视图,然后 2)系统为这些视图收集所有 UIGestureRecognizer,然后 3)它尝试将触摸传递给手势识别器系统并检查哪些手势识别器会收到触摸并且应该失败。 因此,在您的情况下,您在步骤 1) 收集的层次结构中没有底层视图 DragGestureView。因此,只有 TwoFingerNavigationView 的手势识别器在工作。

您的解决方案是将两个手势识别器放在一个 UIView 中。它们将同时出现在步骤 2) 中,并且在您的实施之后将同时进行。

【讨论】:

不错!棒极了!我一定会试一试的!非常感谢!

以上是关于将 SwiftUI 与多个 UIGestureRecognizer 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

将多个子视图传递给 SwiftUI 中的视图

SwiftUI-将@State变量传递给多个视图麻烦

SwiftUI 后台刷新多个 Section 导致 global index in collection view 与实际不匹配问题的解决

SwiftUI 后台刷新多个 Section 导致 global index in collection view 与实际不匹配问题的解决

极简示例揭示 SwiftUI 中 @ObservedObject 与 @StateObject 状态的关键区别

极简示例揭示 SwiftUI 中 @ObservedObject 与 @StateObject 状态的关键区别