Retrieve tapped UIButton in a running animation which can be slightly off the screen


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).

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

<a href="https://i.stack.imgur.com/KQIV8.png" rel="nofollow"><img alt="enter image description here" class="b-lazy" data-src="https://i.stack.imgur.com/KQIV8.png" data-original="https://i.stack.imgur.com/KQIV8.png" src="https://etrip.eimg.top/images/2019/05/07/timg.gif" /></a>


<em>Original Answer:</em>

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.

<hr />

<em>Edited Answer:</em>

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 } }


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") } }


  • conflict scrolling scrollview and tableview
  • Get Height of Table Contents in Swift
  • How to change bottom layout constraint in iOS, Swift
  • NSLayoutConsstraint constant not affecting view after UITapGestureRecognizer was tapped
  • UIViewControllerAnimatedTransitioning with Safe Area Insets on iPhone X
  • Order CSS Styles with Transitions and JavaScript
  • Create a marker on the edge of a circle using jQuery/Javascript/GM
  • How to count how many data point inside a region using 'segments'?
  • Find root of a function in a given interval
  • UIButton is not clickable after first click
  • #selector' refers to a method that is not exposed to Objective-C swift 3
  • Plotting conditional density of prediction after linear regression
  • How to configure email address for a user in Microsoft Azure AD?
  • Fake http server for testing purpose
  • Cannot set password with DirectoryEntry.Invoke when user is created in AD using ASP.NET C#
  • application closes after closing interstitial ad
  • iAd works in iPhone but not iPad
  • How to store result of stored procedure in a variable using SQL Server
  • Displaying iOS iAds only to supported countries
  • Pass nested C++ vector as built-in style multi-dimensional array
  • 2-table interaction: insert, get result, insert
  • How to write string.Contains(someText) in expression Tree
  • What do I do with this error when I run tests in rails?
  • Multiple versions of iTunesArtwork in one project?
  • NUnit 3.0 TestCase const custom object arguments
  • Bash if statement with multiple conditions
  • as3-flash: any way to access all the instances placed in different frames from document class?
  • Can't delete or rename original file after resizing
  • Jquery popup on mouse over of calendar control
  • D3 get axis values on zoom event
  • Copy to all folders batch file?
  • wxPython: displaying multiple widgets in same frame
  • Time complexity of a program which involves multiple variables
  • OpenGL 3.3 on Mac OSX El Capitan with LWJGL
  • R - Combining Columns to String Based on Logical Match
  • How to set/get protobuf's extension field in Go?
  • How to show dropdown in excel using jrxml (jasper api)?
  • embed rChart in Markdown
  • Observable and ngFor in Angular 2
  • Unable to use reactive element in my shiny app