Search code examples
iosuiimageviewcore-animationswift4

How to detect a tap on an UIImageView while it is in the process of animation?


I try to detect a tap on an UIImageView while it is in the process of animation, but it does't work.

What I do (swift 4):

added UIImageView via StoryBoard:

@IBOutlet weak var myImageView: UIImageView!

  doing animation:

override func viewWillAppear (_ animated: Bool) {
        super.viewWillAppear (animated)

        myImageView.center.y + = view.bounds.height

    }


    override func viewDidAppear (_ animated: Bool) {
        super.viewDidAppear (animated)

        UIView.animate (withDuration: 10, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y - = self.view.bounds.height
        })
    }

try to detect the tap:

override func viewDidLoad () {
        super.viewDidLoad ()

        let gestureSwift2AndHigher = UITapGestureRecognizer (target: self, action: #selector (self.actionUITapGestureRecognizer))
        myImageView.isUserInteractionEnabled = true
        myImageView.addGestureRecognizer (gestureSwift2AndHigher)

    }


    @objc func actionUITapGestureRecognizer () {

        print ("actionUITapGestureRecognizer - works!")

    }

Please, before voting for a question, make sure that there are no normally formulated answers to such questions, understandable to the beginner and written in swift above version 2, so I can not apply them for my case.

Studying this problem, I realized that it is necessary to also tweak the frame !? But this is still difficult for me. Tell me, please, what I need to add or change in the code below.

Thank you for your help.

class ExampleViewController: UIViewController {

    @IBOutlet weak var myImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // action by tap
        let gestureSwift2AndHigher = UITapGestureRecognizer(target: self, action:  #selector (self.actionUITapGestureRecognizer))
        myImageView.isUserInteractionEnabled = true
        myImageView.addGestureRecognizer(gestureSwift2AndHigher)

    }

    // action by tap
    @objc func actionUITapGestureRecognizer (){

        print("actionUITapGestureRecognizer - works!") // !!! IT IS DOES NOT WORK !!!

    }

    // hide UIImageView before appear
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        myImageView.center.y += view.bounds.height

    }

    // show UIImageView after appear with animation
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        UIView.animate(withDuration: 10, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y -= self.view.bounds.height
        })
    }
}

Solution

  • You CANNOT do what you want using UITapGestureRecognizer because it uses frame based detection and detects if a touch was inside your view by checking against its frame..

    The problem with that, is that animations already set the view's final frame before the animation even begins.. then it animates a snapshot of your view into position before showing your real view again..

    Therefore, if you were to tap the final position of your animation, you'd see your tap gesture get hit even though your view doesn't seem like it's there yet.. You can see that in the following image:

    https://i.sstatic.net/jEEtc.png

    (Left-Side is view-hierarchy inspector)..(Right-Side is the simulator animating).

    To solve the tapping issue, you can try some sketchy code (but works):

    import UIKit
    
    protocol AnimationTouchDelegate {
        func onViewTapped(view: UIView)
    }
    
    protocol AniTouchable {
        var animationTouchDelegate: AnimationTouchDelegate? {
            get
            set
        }
    }
    
    extension UIView : AniTouchable {
        private struct Internal {
            static var key: String = "AniTouchable"
        }
    
        private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
            return view.subviews.flatMap { subView -> [T] in
                var result = getAllSubviews(view: subView) as [T]
                if let view = subView as? T {
                    result.append(view)
                }
                return result
            }
        }
    
        private func getAllSubviews<T: UIView>() -> [T] {
            return UIView.getAllSubviews(view: self) as [T]
        }
    
        var animationTouchDelegate: AnimationTouchDelegate? {
            get {
                return objc_getAssociatedObject(self, &Internal.key) as? AnimationTouchDelegate
            }
    
            set {
                objc_setAssociatedObject(self, &Internal.key, newValue, .OBJC_ASSOCIATION_ASSIGN)
            }
        }
    
    
        override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let touchLocation = touch.location(in: self)
    
            var didTouch: Bool = false
            let views = self.getAllSubviews() as [UIView]
            for view in views {
                if view.layer.presentation()?.hitTest(touchLocation) != nil {
                    if let delegate = view.animationTouchDelegate {
                        didTouch = true
                        delegate.onViewTapped(view: view)
                    }
                }
            }
    
            if !didTouch {
                super.touchesBegan(touches, with: event)
            }
        }
    }
    
    class ViewController : UIViewController, AnimationTouchDelegate {
    
        @IBOutlet weak var myImageView: UIImageView!
    
        deinit {
            self.myImageView.animationTouchDelegate = nil
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.myImageView.isUserInteractionEnabled = true
            self.myImageView.animationTouchDelegate = self
        }
    
        func onViewTapped(view: UIView) {
            print("Works!")
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            myImageView.center.y += view.bounds.height
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
                self.myImageView.center.y -= self.view.bounds.height
            })
        }
    }
    

    It works by overriding touchesBegan on the UIView and then checking to see if any of the touches landed inside that view.

    A MUCH better approach would be to just do it in the UIViewController instead..

    import UIKit
    
    protocol AnimationTouchDelegate : class {
        func onViewTapped(view: UIView)
    }
    
    extension UIView {
        private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
            return view.subviews.flatMap { subView -> [T] in
                var result = getAllSubviews(view: subView) as [T]
                if let view = subView as? T {
                    result.append(view)
                }
                return result
            }
        }
    
        func getAllSubviews<T: UIView>() -> [T] {
            return UIView.getAllSubviews(view: self) as [T]
        }
    }
    
    class ViewController : UIViewController, AnimationTouchDelegate {
    
        @IBOutlet weak var myImageView: UIImageView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.myImageView.isUserInteractionEnabled = true
        }
    
        func onViewTapped(view: UIView) {
            print("Works!")
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            myImageView.center.y += view.bounds.height
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
                self.myImageView.center.y -= self.view.bounds.height
            })
        }
    
        override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else { return }
            let touchLocation = touch.location(in: self.view)
    
            var didTouch: Bool = false
            for view in self.view.getAllSubviews() {
                if view.isUserInteractionEnabled && !view.isHidden && view.alpha > 0.0 && view.layer.presentation()?.hitTest(touchLocation) != nil {
    
                    didTouch = true
                    self.onViewTapped(view: view)
                }
            }
    
            if !didTouch {
                super.touchesBegan(touches, with: event)
            }
        }
    }