Home > Software engineering >  Processing touches on moving/ animating UiViews
Processing touches on moving/ animating UiViews

Time:10-08

I currently have the problem that touches are not always identified correctly, My goal is to have 3 gestures,The 3 gestures are

  1. A user can tap on a view and the tap gets recognised,
  2. A user can double tap on a view and the double tap is recognised,
  3. A user can move their finger on the screen and if a view is below it a tab is recognised.

However I have multiple views all animating constantly and they may overlap, Currently I sort views by size and have the smallest views on top of larger views. And I typically get an issue that UIViews are not recognised when tapping on them. In particular double taps, swiping seems to work fine most of the time however the whole experience is very inconsistent.

The current code I'm using to solve the problem is:

            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: $0) }
                    drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
                    setNeedsDisplay()
                }
            }

            func drawBoxs(with predictions: [FinderrItem]) {
                var newBoxes = Set(predictions)
                var views = subviews.compactMap { $0 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 { $0.removeFromSuperview() }
                newBoxes.forEach { self.createLabelAndBox(prediction: $0) }
                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 { $0.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 { $0.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 { $0 < $1 }
                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
                }
            }

CodePudding user response:

By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:

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