处理移动/动画 UiView 的接触

Processing touches on moving/ animating UiViews

我目前遇到的问题是触摸并不总是被正确识别, 我的目标是拥有3个手势,这3个手势是

  1. 用户可以点击一个视图,点击会被识别,
  2. 用户可以双击一个视图并且双击被识别,
  3. 用户可以在屏幕上移动手指,如果视图在屏幕下方 一个标签被识别。

但是我有多个视图,它们都在不断地动画化,它们可能会重叠, 目前,我按大小对视图进行排序,并将最小的视图放在较大的视图之上。 我通常会遇到一个问题,即在点击它们时无法识别 UIView。尤其是双击,滑动似乎大部分时间都很好,但整个体验非常不一致。

我目前用来解决问题的代码是:

            class FinderrBoxView: UIView {

            private var lastBox: String?

            private var throttleDelay = 0.01

            private var processQueue = DispatchQueue(label: "com.finderr.FinderrBoxView")


            public var predictedObjects: [FinderrItem] = [] {
                didSet {
                    predictedObjects.forEach { self.checkIfBoxIntersectCentre(prediction: [=10=]) }
                    drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
                    setNeedsDisplay()
                }
            }

            func drawBoxs(with predictions: [FinderrItem]) {
                var newBoxes = Set(predictions)
                var views = subviews.compactMap { [=10=] as? BoxView }
                views = views.filter { view in

                    guard let closest = newBoxes.sorted(by: { x, y in
                        let xd = FinderrBoxView.distanceBetweenBoxes(view.frame, x.box)
                        let yd = FinderrBoxView.distanceBetweenBoxes(view.frame, y.box)

                        return xd < yd
                    }).first else { return false }

                    if FinderrBoxView.updateOrCreateNewBox(view.frame, closest.box)
                    {
                        newBoxes.remove(closest)
                        UIView.animate(withDuration: self.throttleDelay, delay: 0, options: .curveLinear, animations: {
                            view.frame = closest.box
                        }, completion: nil)

                        return false
                    } else {
                        return true
                    }
                }

                views.forEach { [=10=].removeFromSuperview() }
                newBoxes.forEach { self.createLabelAndBox(prediction: [=10=]) }
                accessibilityElements = subviews
            }

            func update(with predictions: [FinderrItem]) {
                var newBoxes = Set(predictions)
                var viewsToRemove = [UIView]()
                for view in subviews {
                    var shouldRemoveView = true
                    for box in predictions {
                        if FinderrBoxView.updateOrCreateNewBox(view.frame, box.box)
                        {
                            UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear, animations: {
                                view.frame = box.box
                            }, completion: nil)
                            shouldRemoveView = false
                            newBoxes.remove(box)
                        }
                    }
                    if shouldRemoveView {
                        viewsToRemove.append(view)
                    }
                }
                viewsToRemove.forEach { [=10=].removeFromSuperview() }

                for prediction in newBoxes {
                    createLabelAndBox(prediction: prediction)
                }
                accessibilityElements = subviews
            }

            func checkIfBoxIntersectCentre(prediction: FinderrItem) {
                let centreX = center.x
                let centreY = center.y
                let maxX = prediction.box.maxX
                let minX = prediction.box.midX
                let maxY = prediction.box.maxY
                let minY = prediction.box.minY
                if centreX >= minX, centreX <= maxX, centreY >= minY, centreY <= maxY {
            //        NotificationCenter.default.post(name: .centreIntersectsWithBox, object: prediction.name)
                }
            }

            func removeAllSubviews() {
                UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear) {
                    for i in self.subviews {
                        i.frame = CGRect(x: i.frame.midX, y: i.frame.midY, width: 0, height: 0)
                    }
                } completion: { _ in
                    self.subviews.forEach { [=10=].removeFromSuperview() }
                }
            }

            static func getDistanceFromCloseBbox(touchAt p1: CGPoint, items: [FinderrItem]) -> Float {
                var boxCenters = [Float]()
                for i in items {
                    let distance = Float(sqrt(pow(i.box.midX - p1.x, 2) + pow(i.box.midY - p1.y, 2)))
                    boxCenters.append(distance)
                }
                boxCenters = boxCenters.sorted { [=10=] <  }
                return boxCenters.first ?? 0.0
            }

            static func sortBoxByeSize(_ items: [FinderrItem]) -> [FinderrItem] {
                return items.sorted { i, j -> Bool in
                    let iC = sqrt(pow(i.box.height, 2) + pow(i.box.width, 2))
                    let jC = sqrt(pow(j.box.height, 2) + pow(j.box.width, 2))
                    return iC > jC
                }
            }

            static func updateOrCreateNewBox(_ box1: CGRect, _ box2: CGRect) -> Bool {
                let distance = sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2))
                print(distance)
                return distance < 50
            }

            static func distanceBetweenBoxes(_ box1: CGRect, _ box2: CGRect) -> Float {
                return Float(sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2)))
            }

            func createLabelAndBox(prediction: FinderrItem) {
                let bgRect = prediction.box
                let boxView = BoxView(frame: bgRect ,itemName: "box")
                addSubview(boxView)
            }

            @objc func handleTap(_ sender: UITapGestureRecognizer) {
                // handling code
            //    NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
            }

            override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
                processTouches(touches, with: event)
            }


            override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
                processTouches(touches, with: event)
            }

            func processTouches(_ touches: Set<UITouch>, with event: UIEvent?) {
                if UIAccessibility.isVoiceOverRunning { return }
                if predictedObjects.count == 0 { return }
                if let touch = touches.first {
                    let hitView = hitTest(touch.location(in: self), with: event)

                    if hitView?.accessibilityLabel == lastBox { return }
                    lastBox = hitView?.accessibilityLabel
                    
                    guard let boxView = hitView as? BoxView else {
                        return
                    }

                    UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear) {
                        boxView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)

                    } completion: { _ in
                        UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: {
                            boxView.backgroundColor = UIColor.clear
                        }, completion: nil)
                    }

                }
            }

            }

            class BoxView: UIView {
                let id = UUID()
                var itemName: String

                init(frame: CGRect, itemName: String) {
                self.itemName = itemName
                super.init(frame: frame)
                if !UIAccessibility.isVoiceOverRunning {
                    let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
                        target: self,
                        singleAction: #selector(handleDoubleTapGesture),
                        doubleAction: #selector(handleDoubleTapGesture)
                    )
                    addGestureRecognizer(singleDoubleTapRecognizer)
                }

                }

            @objc func navigateAction() -> Bool {
            //    NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
                return true
            }

            required init?(coder aDecoder: NSCoder) {
                itemName = "error aDecoder"
                super.init(coder: aDecoder)
            }

            @objc func handleDoubleTapGesture(_: UITapGestureRecognizer) {
                // handling code
            //    NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
            }

            }

            public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
                var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
                public var timeout: TimeInterval = 0.5 {
                    didSet {
                        targetDelegate.timeout = timeout
                    }
                }

                public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
                    targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
                    super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
                }
            }

            class SingleDoubleTapGestureRecognizerDelegate: NSObject {
                weak var target: AnyObject?
                var singleAction: Selector
                var doubleAction: Selector
                var timeout: TimeInterval = 0.5
                var tapCount = 0
                var workItem: DispatchWorkItem?

                init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
                    self.target = target
                    self.singleAction = singleAction
                    self.doubleAction = doubleAction
                }

                @objc func recognizerAction(recognizer: UITapGestureRecognizer) {
                    tapCount += 1
                    if tapCount == 1 {
                        workItem = DispatchWorkItem { [weak self] in
                            guard let weakSelf = self else { return }
                            weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
                            weakSelf.tapCount = 0
                        }
                        DispatchQueue.main.asyncAfter(
                            deadline: .now() + timeout,
                            execute: workItem!
                        )
                    } else {
                        workItem?.cancel()
                        DispatchQueue.main.async { [weak self] in
                            guard let weakSelf = self else { return }
                            weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
                            weakSelf.tapCount = 0
                        }
                    }
                }
            }

            class FinderrItem: Equatable, Hashable {
                var box: CGRect
                init(
                     box: CGRect)
                {
                    self.box = box
                }

                func hash(into hasher: inout Hasher) {
                    hasher.combine(Float(box.origin.x))
                    hasher.combine(Float(box.origin.y))
                    hasher.combine(Float(box.width))
                    hasher.combine(Float(box.height))
                    hasher.combine(Float(box.minX))
                    hasher.combine(Float(box.maxY))
                }

                static func == (lhs: FinderrItem, rhs: FinderrItem) -> Bool {
                    return lhs.box == rhs.box
                }
            }

默认情况下,视图对象会在动画“运行中”时阻止用户交互。您需要使用一种“长格式”动画方法,并传入选项 .allowUserInteraction。像这样:

UIView.animate(withDuration: 0.5,
  delay: 0.0,
  options: .allowUserInteraction,
  animations: {
    myView.alpha = 0.5
})