So I have a tableviewController called SettingsViewController, and it has the following touchesEnded function:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("A")
super.touchesEnded(touches, with: event)
print("B")
if let touch = touches.first {
print("C")
let touchLocation = touch.location(in: view)
// 290 because the width of the view is 414, and the SettingsViewController width gets set to 0.7 * the view width in SlideInTransition. 0.7 * 414 is 289.8
if touchLocation.x > 290 {
dismiss(animated: true)
}
}
}
I made the print statement to see if it was being called, which it is not. This view controller is presented with a 'menu-esque' slide in custom transition. Is it possible that the bounds of the UIView of the new tablevc displayed with a custom transition is the problem somehow? Regardless, I have tried implementing super.touchesEnded, or adding all the override touches functions and tapped all over the screen with each simulation, but the touchesEnded never gets called. Any idea what's wrong and how to fix it? Thanks
I'm going to guess that you're putting the touchesEnded() func in your table view controller ... which is not going to work. The table view itself is consuming the touches.
This might work for you...
Add this UITableView subclass to your project:
class TouchableTableView: UITableView {
var callback: (() -> ())?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("A")
super.touchesEnded(touches, with: event)
print("B")
if let touch = touches.first {
print("C")
let touchLocation = touch.location(in: self)
// not clear where you're trying to check for touch, so this may need to be changed
// 290 because the width of the view is 414, and the SettingsViewController width gets set to 0.7 * the view width in SlideInTransition. 0.7 * 414 is 289.8
if touchLocation.x > 290 {
// this code is in a UITableView subclass,
// so we can't call dismiss() from here
//dismiss(animated: true)
// instead, we tell the Controller to take action
callback?()
}
}
}
}
Then, in Storyboard, select the Table View in your Table View Controller and assign its Custom Class to TouchableTableView:
Now, touchesEnded() will be called in the custom TouchableTableView class (unless you tap on an interactive UI element in the cell, such as a button).
We then use the "callback" closure to tell the Controller about the touch. So, in your UITableViewController, add this in viewDidLoad():
class ExampleTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// we're in a UITableViewController,
// so make sure the tableView is our TouchableTableView class
// if it is, setup the closure
if let tv = tableView as? TouchableTableView {
tv.callback = {
// here is where we would, for example, call dismiss()
self.dismiss(animated: true, completion: nil)
}
}
}
}
Related
I have a view controller where I can add several subviews with a LongPressGesture. For each subview I provide a TapGesture that should open a popover for this view (see picture below).
My problem is, that I can only open the popover for the last subview I added. So why can't I interact with the other subviews anymore?
This is my first App in Swift, so it would be nice if someone could help me.
Some code for you:
This is the LongPressGesture on the root view controller that creates a new subview.
#IBAction func onLongPress(_ gesture : UILongPressGestureRecognizer) {
let position: CGPoint = gesture.location(in: view)
if(gesture.state == .began) {
let subview = MySubview(position: position)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
subview.addGestureRecognizer(tapGesture)
view.addSubview(subview)
}
else if (gesture.state == .ended) {
let subview = self.view.subviews.last
self.openContextMenu(for: subview)
}
}
ViewController with subviews:-
When reacting to an event (long press in your case) only the first responder will handle the event. When adding subviews to a view, all the subviews are added one in front of another. So the last added subview will handle the event - if it is listening for the event. If you need a specific subview to handle the event, you can bring it to front as
parentView.bringSubviewToFront(view: subView)
Alternatively, you can also disable other subviews as
subView.isUserInteractionEnabled = false
Ok, I found a solution for my problem. The good thing is, that it only detects touches in the frames of the subviews.
In my root view controller I added:
var isPanGesture = false
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
isPanGesture = true
if let subview = touches.first!.view as? MySubview {
let position = touches.first!.location(in: self.view)
link.center = position
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let subview = touches.first!.view as? MySubview {
if(!isPanGesture) {
self.openContextMenu(for: subview)
}
isPanGesture = false
}
}
I have created a custom control for Uber like OTP TextField. I want to consume the touch in my control and not let it propagate through the UIResponder Chain. So I have overridden the methods as described in apple documentation.
extension BVAPasswordTextField {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
becomeFirstResponder()
}
override func touchesMoved(Set<UITouch>, with: UIEvent?) {
}
override func touchesEnded(Set<UITouch>, with: UIEvent?) {
}
override func touchesCancelled(Set<UITouch>, with: UIEvent?) {
}
override func touchesEstimatedPropertiesUpdated(Set<UITouch>) {
}
}
Now in some view controller I want to dismiss the keyboard when the user taps anywhere outside my custom control.
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecogniser = UITapGestureRecognizer(target: self, action: #selector(ViewController.backgroundTapped))
tapGestureRecogniser.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGestureRecogniser)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func backgroundTapped() {
self.view.endEditing(true)
}
But whenever I tap on the textfield backgroundTapped also gets called.
Note:- It is a control where based on enum values you can create different UI Components for taking input. So this control can be shared among the whole team... I won't be the only guy using it.... So I want it to behave exactly like UITextfield in touch scenario
You can handle the tap gesture using UITapGestureRecognizerDelegate. It allows you to decide whether or not gesture should begin. In your case, it should be done basing on the location of the touch.
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
// return true is location of touch is outside our textField
return !textField.frame.contains(location)
}
}
Just make sure you've set a delegate for your gesture at viewDidLoad
tapGestureRecogniser.delegate = self
UPDATE
If I got your setup right, you have a view and some subviews that are used for input. And you want to resignFirstResponder when this super-view is touched. In that case you can use gestureRecognizerShouldBegin like that
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.view)
for subview in self.view.subviews {
if subview.frame.contains(location) {
return false
}
}
return true
}
In my opinion, if you want to handle some view interaction in a specific way, you need to do it using that specific view. And what you're doing now feels like trying to change behavior on one view, using another view.
I have a modal with a UITableView in it. I want to dismiss the modal if anywhere on the screen is touched except for the UITableViewCells inside the table view. However, you can't create an outlet for a UITableView cell to reference it in code. Currently I have this code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch? = touches.first
if touch?.view != WHAT TO PUT HERE? {
dismiss(animated: true, completion: nil)
}
}
I'm not sure how to reference the UITableViewCells. How should I do this?
I found a solution that works. I had to resize the tableView to fit the size of its content using this method:
override func viewDidAppear(_ animated: Bool) {
//Resize the tableView height to the height of its content.
tableView.frame.size.height = soundTableView.contentSize.height
//Set the origin of tableView to the bottom of the screen minus a 30px margin
//(this was my tableView's original position before resizing)
tableView.frame.origin.y = self.view.frame.size.height - soundTableView.frame.size.height - 30
}
Then I have this method which dismisses the modal if anywhere but the tableView is touched:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch? = touches.first
if touch?.view != tableView {
dismiss(animated: true, completion: nil)
}
}
The isHighlighted property on UIButtonis mostly analogous to whether the button is pressed or not.
The Apple docs are vague about it:
Controls automatically set and clear this state in response to
appropriate touch events.
https://developer.apple.com/documentation/uikit/uicontrol/1618231-ishighlighted
Which method is the system calling to set this?
It appears to be touchesBegan, because you can override this method to prevent it.
But, surprisingly, if you manually call touchesBegan it doesn't get set (for example, from a superview-- see code below).
So it seems it's not simple as touchesBegan. I've tried overriding every method I could...pressesBegan, pressesChanged, pressesCancelled, pressesEnded, touchesCancelled, beginTracking, continueTracking, hitTest, etc.
Only touchesBegan had any effect.
Here's how I tried calling touchesBegan from a superview:
class MyView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
for view in self.subviews {
if let button = view as? UIButton {
if button.frame.contains(point) {
button.touchesBegan(touches, with: event)
print("\(button.isHighlighted)") //prints false
}
}
}
}
}
Hello all you awesome peeps! I have been having an odd issue where if UIView is animated, in my case rotated, during the animation the subviews touch locations are inaccurate.
For example, if I keep clicking button1, sometimes button2 will be pressed or nothing will be pressed. I've searched on STO and other blogs, trying their methods, but nothing seems to work. My goal is to have buttons clickable when the view is rotating.
I thought it was a UIButton bug or issue, but even with imageViews, and touch delegates or added Gestures, I get the same thing. I feel like something simple, like the presentation.layer or something that needs to be added to the UIView animate function, but I'm kinda at a loss. Hope you can shed some insight! Thank you all in advance.
func updateRotateView ()
{
UIView.animateWithDuration(2,
delay: 0.0,
options: [UIViewAnimationOptions.CurveLinear, UIViewAnimationOptions.AllowUserInteraction],
animations: {
self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
//Test A
//self.button1.frame = self.rotationView.convertRect(self.button1.frame, fromView:self.rotationView)
//self.button1.layer.frame = self.rotationView.layer.convertRect(self.button1.layer.frame, fromLayer: self.rotationView.layer)
}
completion: {finished in })
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
//Test B
let firstTouch = touches.first
let firstHitPoint = firstTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1")
}
if (self.button1.layer.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(firstHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Test C
let lastTouch = touches.first!
let lastHitPoint = lastTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1")
}
if (self.button1.layer.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(lastHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
extension UIButton {
//Test D
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
{
print("hitTest called")
}
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool
{
print("pointInside called")
}
}
Test A -> rotationView.presentationLayer value changes, but the buttons do not, and setting it to an updated frame, is not working as the button1 frame or layer.frame size values do not change.
Test B/C -> UIButton does not respond to touch delegates, but even with imageViews does not hit accurately. Also did try Gestures with same result.
Test D -> Hitpoint does get called, but most likely on the wrong button as well. Point inside only gets called few times and if I am able to press the correct button, but again very inaccurate.
Because you are animating a view. You should know that there is a presentation layer and a model layer when animating.
Once self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
is called in the animation block, the transform of rotationView has already been set, and the animation you see is just the presentation layer(It is an id type but it should be CALayer).