UIButton: manually calling touchesBegan does not set isHighlighted - ios

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

Related

iOS Swift: Propagates Swipe Events from nester controller to underneath controller

I have a nested UIViewController that covers half of the screen.
Under this controller there is the parent ViewController.
I would like to propagate only the lateral swipe events to the bottom (parent) controller.
I think I should use the following func
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return (my test)
})
}
but I also know that this func should be applied to a UIView, not to a UIViewController. So I'm stuck, and I don't know how to proceed.
You can create
class SomeView:UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return true/false
}
}
and assign it to the child's vc'view in IB , you can also implement this inside the vc
override func touchesBegan(_ touches: Set<AnyHashable>, withEvent event: UIEvent) {
var touch: UITouch? = touches.first
//location is relative to the current view
// do something with the touched point
if touch?.view == childVC.view {
}
}
Edit:
to propagate swipe actions to parent add a delegate and swipe actions to the child and when triggered notify the parent with the delegate

Preventing touch passing from UIControl to ParentView

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.

3D Touch force gesture activates cell tap on touch end

I have subclassed a UITableView in my app so that I can intercept touch events. I am using this to allow me to provide 3D Touch gestures on the entire view (including on top of the table view).
This works great, however the problem is that using 3D Touch on one of the cells and then releasing your finger activates the cell tap.
I need to only activate the cell tap if there is no force exerted. I should explain that I am fading an image in gradually over the entire screen as you apply pressure.
Here is my subclass:
protocol PassTouchesTableViewDelegate {
func touchMoved(touches: Set<UITouch>)
func touchEnded(touches: Set<UITouch>)
}
class PassTouchesTableView: UITableView {
var delegatePass: PassTouchesTableViewDelegate?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesMoved(touches, withEvent: event)
self.delegatePass?.touchMoved(touches)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
self.delegatePass?.touchEnded(touches)
}
}
And here are the methods I'm calling from my view controller when the touches end and move:
internal func touchMoved(touches: Set<UITouch>) {
let touch = touches.first
self.pressureImageView.alpha = (touch!.force / 2) - 1.0
}
internal func touchEnded(touches: Set<UITouch>) {
UIView.animateWithDuration(0.2, animations: {
self.pressureImageView.alpha = 0.0
})
}
You could create a boolean named isForceTouch which is set to false in touchesBegan, and then once force touch is detected, set it to true. Then in didSelectRowAtIndexPath just return false if isForceTouch is true. It may need tweaking but that should work.

UIButton touch location inaccurate when UIView Animating/Rotating

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

Detecting tap on a UITextView only detects cancellations [duplicate]

This question already has answers here:
How do you make a UITextView detect link part in the text and still have userInteractionDisabled?
(4 answers)
Closed 4 years ago.
I have been struggling to detect a tap on a UITextView with Swift.
My UITextViews are in a table, I must be able to detect links and press them, those links length are unknown.
Also if I tap on the cell, that I don't tap on a link, I want push a new UIViewController on my navigation controller stack.
I tried to create my own textview to overwrite the touchesCancelled, but it wasn't a success. It detects the cancellation which isn't considered a tap on the real device.
The bug doesn't occur in the simulator, but it seems I can't tap on the real device, only long press will work.
class LinkTextView: UITextView {
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.textViewWasTapped(self)
}
}
I tried adding directly a gesture recognizer. I didn't have any success there either. It doesn't call the gesture recognizer at all.
I added the UIGestureReconizer delegate to my UIViewController and those lines in my
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var singleTap : UIGestureRecognizer = UIGestureRecognizer(target: self, action: "tapTextView:")
singleTap.delegate = self
cell.mTextOutlet.attributedText = myMutableString
cell.mTextOutlet.addGestureRecognizer(singleTap)
In my LinkTextView class :
class LinkTextView: UITextView {
func tapTextView(stapGesture: UIGestureRecognizer){
println("TAPPING1")
}
}
I looked the forums and found this post : post. It suggests to use CHHLinkTextView. I tried to use it but what I want is to detect the link automatically, which normal uitextview actually does.
I did try using the checkBox in interface builder to parse links with the CHHLinkTextView, but it doesn't work. I didn't see anything in the documentation suggesting it could be done.
How should I proceed ?
You're very close with your first attempt in subclassing UITextView, but instead of overriding only touchesCancelled, you'll want to override all of the touches* methods, i.e.
touchesBegan
touchesMoved
touchesEnded
touchesCancelled
In the overridden methods, send the touch down the responder chain by getting the textView's nextResponder(), check that it isn't nil, and call the method on the next responder.
The reason this works is because the UITextView will intercept the touch if it's on a URL and the UIResponder methods will never be called -- try it for yourself by implementing textView:shouldInteractWithURL:inRange in your tableViewCell or wherever, and you'll see that it's called before any touch events are passed along.
This should be the minimum for what you're trying to do (it's repetitive but short):
class PassThroughTextView: UITextView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesBegan(touches, with: event)
} else {
super.touchesBegan(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesEnded(touches, with: event)
} else {
super.touchesEnded(touches, with: event)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesCancelled(touches, with: event)
} else {
super.touchesCancelled(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesMoved(touches, with: event)
} else {
super.touchesMoved(touches, with: event)
}
}
}
// In case you want to do anything with the URL or textView in your tableViewCell...
class TableViewCell: UITableViewCell, UITextViewDelegate {
#IBOutlet var textView: PassThroughTextView!
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
println("textView shouldInteractWithURL")
return true
}
}
I did as Ralfonso said without success, but it helped me realize I had a gesture recognizer conflict. Turns out I didn't have to override all 4 methods of the UITextView, I just override the touchesBegan.
What I was doing in the viewDidLoad was add a gesture recognizer to dismiss the keyboard :
var tapOutTextField: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
self.view.addGestureRecognizer(tapOutTextField)
What didn't make sense and sent me in a wrong direction was that touchesBegan was actually called after a delay (so was touchCancelled), and I could not get the same behaviour as a UIButton. All that because of this gesture recognizer conflict.

Resources