3

This is what the code (below) can look like:

import UIKit

class TornadoButton: UIButton {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        let prespt = self.superview!.layer.convert(suppt, to: pres)

        return super.hitTest(prespt, with: event)
    }
}

class TestViewController: UIViewController {

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

        let greySubview = UIView()
        greySubview.backgroundColor = .red
        greySubview.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(greySubview)

        greySubview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        greySubview.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        greySubview.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
        greySubview.heightAnchor.constraint(equalTo: greySubview.widthAnchor).isActive = true

        let button = TornadoButton()
        greySubview.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.widthAnchor.constraint(equalTo: greySubview.widthAnchor, multiplier: 0.09).isActive = true
        button.heightAnchor.constraint(equalTo: greySubview.heightAnchor, multiplier: 0.2).isActive = true

        //below constrains are needed, else the origin of the UIBezierPath is wrong
        button.centerXAnchor.constraint(equalTo: greySubview.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: greySubview.centerYAnchor, constant: self.view.frame.height * 0.35).isActive = true
        self.view.layoutIfNeeded()

        button.addTarget(self, action: #selector(tappedOnCard(_:)), for: .touchUpInside)

        let circlePath = UIBezierPath(arcCenter: greySubview.frame.origin, radius: CGFloat(greySubview.frame.width * 0.5), startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)

        let orbit = CAKeyframeAnimation(keyPath: "position")
        orbit.duration = 12
        orbit.path = circlePath.cgPath
        orbit.isAdditive = true
        orbit.repeatCount = Float.greatestFiniteMagnitude
        orbit.calculationMode = kCAAnimationPaced
        orbit.rotationMode = kCAAnimationRotateAuto

        button.layer.add(orbit, forKey: "orbit")
        button.backgroundColor = .blue

        let gr = UITapGestureRecognizer(target: self, action: #selector(onTap(gesture:)))
        greySubview.addGestureRecognizer(gr)
    }

    @objc func onTap(gesture:UITapGestureRecognizer) {
        let p = gesture.location(in: gesture.view)
        let v = gesture.view?.hitTest(p, with: nil)
    }

    @IBAction func tappedOnCard(_ sender: UIButton) {
        print(sender)
    }
}

This almost works, BUT:

I can retrieve the button if the button is 100% visible on the screen. If it is, for example, 50% visible (and 50% off the screen (look picture below)), I do not retrieve the buttons tap (aka not retrieve the print).

How can I retrieve the button while it is in a running animation and it can be slightly off the screen?

enter image description here

maxwell
  • 3,788
  • 6
  • 26
  • 40
J. Doe
  • 12,159
  • 9
  • 60
  • 114

2 Answers2

2

Original Answer:

It is very likely that the UITapGestureRecognizer in greySubview is consuming the touch and not letting it pass through to the button. Try this:

gr.cancelsTouchesInView = NO;

before adding the UITapGestureRecognizer to greySubview.


Edited Answer:

Please ignore my earlier answer and revert the change if you did it. My original answer did not make sense because in your case, the button is the subview of the grayView and cancelsTouchesInView works upwards in the view hierarchy, not downwards.

After a lot of digging, I was able to figure out why this was happening. It was because of incorrect hit testing in the TornadoButton during animation. Since you are animating the button, you will need to override the hitTest and the pointInside methods of the TornadoButton so that they account for the animated position instead of the actual position of the button when hit testing.

The default implementation of the pointInside method does not take into account the animated presentation layer of the button, and just tries to check for the touch point within the rect (which will fail because the touch was somewhere else).

The following implementations of the methods seem to do the trick:

class TornadoButton: UIButton{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        let prespt = self.superview!.layer.convert(suppt, to: pres)
        if (pres.hitTest(suppt)) != nil{
            return self
        }
        return super.hitTest(prespt, with: event)
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        return (pres.hitTest(suppt)) != nil
    }
}
aksh1t
  • 5,410
  • 1
  • 37
  • 55
  • Tried it without luck. – J. Doe Sep 26 '17 at 17:47
  • @J.Doe Please check out my edited answer, it should do the trick (spent 2 hours trying to figure out the exact thing) :D – aksh1t Sep 26 '17 at 20:15
  • Your 2 hours will be rewarded with some reputation, it worked :D. "You may award your bounty in 19 hours.". Patience... :) – J. Doe Sep 26 '17 at 21:34
  • Good to hear that! This was a great question though, I've had my fair share of hit test problems when dealing with irregularly shaped UIButtons, but this was my first experience with animated buttons and I got to learn how to deal with them too. – aksh1t Sep 26 '17 at 21:53
  • Hey, it is me again. I am using your code and it works most of the time :). I have a new question with a bounty award of 100 in it, I hope you can help me out. The question is here: https://stackoverflow.com/questions/46574223/get-tap-event-for-uibutton-in-uicontrol-rotatable-view. Check the comment section with Nathan. I can do everything but I can not indicate a unique button. Every button has tags, but if the wheel rotated, I am unable to get the right UIButton on a touch down. I hope you can help me out, I think it is the last step to the answer. – J. Doe Oct 15 '17 at 22:17
  • Make sure you downloaded the most recent project which is in the comment section with Nathan. With your knowledge of hitTests, I hope you can identify the button at the clicked point. – J. Doe Oct 15 '17 at 22:23
  • @J.Doe I had a chance to look at the other project and I've answered there. Do tell if the solution works/does not work. That was a really interesting question as well! – aksh1t Oct 16 '17 at 05:40
0

I don't quite get, why you use buttons for showing a simple rectangle when views would work as well, but anyways... You didn't show us where you add the target to your button.

button.addTarget(self, action: #selector(ViewController.buttonPressed(_:)), for: .touchUpInside) // This is needed that your button 'does something'
button.tag = 1 // This is a tag that you can use to identify which button has been pressed

with the function buttonPressed looking something like this:

@objc func buttonPressed(_ button: UIButton) {
    switch button.tag {
    case 1: print("First button was pressed")
    case 2: print("Second button was pressed")
    default: print("I don't know the button you pressed")
    }
}
Michael
  • 657
  • 5
  • 22
  • This will not work, it will only see button clicks in the middle of the screen, not on the moving uiButtom – J. Doe Sep 24 '17 at 14:44
  • What is `orbit` in your code? If i make it `CAAnimation` then I get `value of type 'CAAnimation' has no member 'rotationMode'` but `button.layer.add` requirers a `CAAnimation` as input... – Michael Sep 24 '17 at 15:00
  • Im sorry: let orbit = CAKeyframeAnimation(keyPath: "position") – J. Doe Sep 24 '17 at 15:03
  • I've tried many things as well. It only worked for me when not adding the button to greyView but to view... – Michael Sep 24 '17 at 15:50
  • I am putting a bounty on this question, so if you have other ideas... :) see my edit for a fix but it is not working correctly. – J. Doe Sep 26 '17 at 12:16