How to prevent UINavigationBar from blocking touches on view - ios

I have a UIView that is partially stuck underneath a UINavigationBar on a UIViewController that's in full screen mode. The UINavigationBar blocks the touches of this view for the portion that it's covering it. I'd like to be able to unblock these touches for said view and have them go through. I've subclassed UINavigationBar with the following:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view.tag == 399)
{
return view;
}
else
{
return nil;
}
}
...where I've tagged the view in question with the number 399. Is it possible to pass through the touches for this view without having a pointer to it (i.e. like how I've tagged it above)? Am a bit confused on how to make this work with the hittest method (or if it's even possible).

Here's a version which doesn't require setting the specific views you'd like to enable underneath. Instead, it lets any touch pass through except if that touch occurs within a UIControl or a view with a UIGestureRecognizer.
import UIKit
/// Passes through all touch events to views behind it, except when the
/// touch occurs in a contained UIControl or view with a gesture
/// recognizer attached
final class PassThroughNavigationBar: UINavigationBar {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard nestedInteractiveViews(in: self, contain: point) else { return false }
return super.point(inside: point, with: event)
}
private func nestedInteractiveViews(in view: UIView, contain point: CGPoint) -> Bool {
if view.isPotentiallyInteractive, view.bounds.contains(convert(point, to: view)) {
return true
}
for subview in view.subviews {
if nestedInteractiveViews(in: subview, contain: point) {
return true
}
}
return false
}
}
fileprivate extension UIView {
var isPotentiallyInteractive: Bool {
guard isUserInteractionEnabled else { return false }
return (isControl || doesContainGestureRecognizer)
}
var isControl: Bool {
return self is UIControl
}
var doesContainGestureRecognizer: Bool {
return !(gestureRecognizers?.isEmpty ?? true)
}
}

Subclass UINavigationBar and override- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event such that it returns NO for the rect where the view you want to receive touches is and YES otherwise.
For example:
UINavigationBar subclass .h:
#property (nonatomic, strong) NSMutableArray *viewsToIgnoreTouchesFor;
UINavigationBar subclass .m:
- (NSMutableArray *)viewsToIgnoreTouchesFor
{
if (!_viewsToIgnoreTouchesFor) {
_viewsToIgnoreTouchesFor = [#[] mutableCopy];
}
return _viewsToIgnoreTouchesFor;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL pointInSide = [super pointInside:point withEvent:event];
for (UIView *view in self.viewsToIgnoreTouchesFor) {
CGPoint convertedPoint = [view convertPoint:point fromView:self];
if ([view pointInside:convertedPoint withEvent:event]) {
pointInSide = NO;
break;
}
}
return pointInSide;
}
In your fullscreen viewController where you have the view behind the navBar add these lines to viewDidLoad
UINavigationBarSubclass *navBar =
(UINavigationBarSubclass*)self.navigationController.navigationBar;
[navBar.viewsToIgnoreTouchesFor addObject:self.buttonBehindBar];
Please note: This will not send touches to the navigationBar, meaning if you add a view which is behind buttons on the navBar the buttons on the navBar will not receive touches.
Swift:
var viewsToIgnore = [UIView]()
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let ignore = viewsToIgnore.first {
let converted = $0.convert(point, from: self)
return $0.point(inside: converted, with: event)
}
return ignore == nil && super.point(inside: point, with: event)
}
See the documentation for more info on pointInside:withEvent:
Also if pointInside:withEvent: does not work how you want, it might be worth trying the code above in hitTest:withEvent: instead.

I modified Tricky's solution to work with SwiftUI as an Extension. Works great to solve this problem. Once you add this code to your codebase all views will be able to capture clicks at the top of the screen.
Also posted this alteration to my blog.
import UIKit
/// Passes through all touch events to views behind it, except when the
/// touch occurs in a contained UIControl or view with a gesture
/// recognizer attached
extension UINavigationBar {
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard nestedInteractiveViews(in: self, contain: point) else { return false }
return super.point(inside: point, with: event)
}
private func nestedInteractiveViews(in view: UIView, contain point: CGPoint) -> Bool {
if view.isPotentiallyInteractive, view.bounds.contains(convert(point, to: view)) {
return true
}
for subview in view.subviews {
if nestedInteractiveViews(in: subview, contain: point) {
return true
}
}
return false
}
}
private extension UIView {
var isPotentiallyInteractive: Bool {
guard isUserInteractionEnabled else { return false }
return (isControl || doesContainGestureRecognizer)
}
var isControl: Bool {
return self is UIControl
}
var doesContainGestureRecognizer: Bool {
return !(gestureRecognizers?.isEmpty ?? true)
}
}

Swift solution for the above mentioned Objective C answer.
class MyNavigationBar: UINavigationBar {
var viewsToIgnoreTouchesFor:[UIView] = []
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var pointInside = super.point(inside: point, with: event)
for each in viewsToIgnoreTouchesFor {
let convertedPoint = each.convert(point, from: self)
if each.point(inside: convertedPoint, with: event) {
pointInside = false
break
}
}
return pointInside
}
}
Now set the views whose touches you want to capture beneath the navigation bar as below from viewDidLoad method or any of your applicable place in code
if let navBar = self.navigationController?.navigationBar as? MyNavigationBar {
navBar.viewsToIgnoreTouchesFor = [btnTest]
}

Excellent answer from #Tricky!
But I've recently noticed that it doesn't work on iPad with the latest iOS. It turned out that any navigation bar on iPad has gesture recognizers built-in since keyboards were introduced.
So I made a simple tweak to make it work again:
/// Passes through all touch events to views behind it, except when the
/// touch occurs in a contained UIControl or view with a gesture
/// recognizer attached
final class PassThroughNavigationBar: UINavigationBar {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard nestedInteractiveViews(in: self, contain: point) else { return false }
return super.point(inside: point, with: event)
}
private func nestedInteractiveViews(in view: UIView, contain point: CGPoint) -> Bool {
if view.isPotentiallyInteractive, view.bounds.contains(convert(point, to: view)) {
return true
}
for subview in view.subviews {
if nestedInteractiveViews(in: subview, contain: point) {
return true
}
}
return false
}
}
fileprivate extension UIView {
var isPotentiallyInteractive: Bool {
guard isUserInteractionEnabled else { return false }
return (isControl || doesContainGestureRecognizer) && !(self is PassThroughNavigationBar)
}
var isControl: Bool {
return self is UIControl
}
var doesContainGestureRecognizer: Bool {
return !(gestureRecognizers?.isEmpty ?? true)
}
}

Related

PDFTron change arrow annotation direction iOS

The default PTArrowCreate class draws arrows pointing to the user's initial tap on the screen. I want arrows to be pointing at the place where user did finish dragging finger.
Please give me a clue how can i achieve this.
There isn't currently a built-in option for this, but you can implement this via subclassing. Arrow annotations are created using the tool PTAnnotCreate, which you can subclass by registering a subclass before the PTDocumentViewController is created:
[PTOverrides overrideClass:[PTArrowCreate class] withClass:[FWArrowCreate class]];
Then swap the head with the tail of the arrow in the subclass as follows:
#interface FWArrowCreate : PTArrowCreate
#end
#implementation FWArrowCreate
-(void)swapStartAndEndPoints
{
CGPoint savedStartPoint = self.startPoint;
self.startPoint = self.endPoint;
self.endPoint = savedStartPoint;
}
-(void)drawRect:(CGRect)rect
{
[self swapStartAndEndPoints];
[super drawRect:rect];
[self swapStartAndEndPoints];
}
- (BOOL)pdfViewCtrl:(PTPDFViewCtrl*)pdfViewCtrl onTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self swapStartAndEndPoints];
BOOL result = [super pdfViewCtrl:pdfViewCtrl onTouchesEnded:touches withEvent:event];
[self swapStartAndEndPoints];
return result;
}
#end
The same answer in Swift:
class MyArrowCreate: PTArrowCreate {
override func draw(_ rect: CGRect) {
swapPoints()
super.draw(rect)
swapPoints()
}
override func pdfViewCtrl(_ pdfViewCtrl: PTPDFViewCtrl, onTouchesEnded touches: Set<UITouch>, with event: UIEvent?) -> Bool {
swapPoints()
let result = super.pdfViewCtrl(pdfViewCtrl, onTouchesEnded: touches, with: event)
swapPoints()
return result
}
private func swapPoints() {
let tmpPoint = startPoint
startPoint = endPoint
endPoint = tmpPoint
}
}

How to catch the click on the button in added XIB view (annotation)

1) I have the ViewController with the MapKit
1.1) I have added some pin's to Map
class ViewController: UIViewController, MKMapViewDelegate
2) I write the new classes for custom pin Callout and Annotation
class CustomPointAnnotation: MKPointAnnotation {
class CustomCalloutView: UIView {
3) I haved created .xib for my custom pin callout
4) I created the button in my .xib, this button must do something, for example
#IBAction func clickTest(sender: AnyObject) {
print("aaaaa")
}
5) This button doesn't work
What is usually practice for this? I want to make my button working.
Button is blue:
All project you can see on Github: https://github.com/genFollowMe1/forStack
Thank you, sorry for English )
for Objective C:
https://github.com/nfarina/calloutview
Update
for swift
subclass the MKAnnotationView for custom call out and override the hitTest: and pointInside: methods.
import UIKit
import MapKit
class CustomCalloutView: MKAnnotationView {
#IBOutlet weak var name: UILabel!
#IBAction func goButton(sender: AnyObject) {
print("button clicked sucessfully")
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if hitView != nil {
superview?.bringSubviewToFront(self)
}
return hitView
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = CGRectContainsPoint(rect, point)
if !isInside {
for view in subviews {
isInside = CGRectContainsPoint(view.frame, point)
if isInside {
break
}
}
}
return isInside
}
}
The way you have to do it's to make a reference of the button from your .xib into the associated .swift file.
In your ViewController you do something like this :
let button:UIButton!
[...]
button.targetForAction("tappedButton:", withSender: self)
func tappedButton() {
print("Taped !")
}

Preventing annotation deselection in MKMapView

I have a situation in my app where I want to disable annotation deselection (other than when selecting another), so when I tap anywhere that is not an annotation view, it should leave the currently selected annotation as is. If I tap on another annotation view, it should select that one and deselect the other.
I was hoping to find something along the lines of a willDeselectAnnotationView in the MKMapViewDelegate or an isDeselected in MKAnnotationView, but there's unfortunately no such thing. I also tried overriding deselectAnnotation in a custom subclass of MKMapView, but it seems the tap triggered deselect doesn't invoke that function.
Is it possible to disable annotation deselection while preserving the ability to select? Thanks!
I've found a way to do it! Make a boolean named something like "allowSelectionChanges", which I just have as global for now. Then use a subclass of MKMapView with this overriden function inside:
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
return allowSelectionChanges
}
Switch this variable to false whenever you want to prevent annotation selection and deselection. It won't affect the user's ability to move around the map!
Here's an example of how this can be used to stop a callout from getting deselected when you tap on it to interact with it. Put this in your MKAnnotationView subclass:
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = CGRectContainsPoint(rect, point)
if !isInside {
for view in self.subviews {
isInside = CGRectContainsPoint(view.frame, point)
if isInside {
allowSelectionChanges = false
return true
}
}
allowSelectionChanges = true
}
return false
}
Ok... I had this issue myself and while #clinton's answer pointed me in the right direction I came up with a solution that doesn't require your MKAnnotationView subclass to know about the mapView's custom property.
Here's my solution written for Swift 3:
public class <#CustomMapViewClass#>: MKMapView {
private var allowSelectionChanges: Bool = true
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return allowSelectionChanges
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let pointInside = super.point(inside: point, with: event)
if !pointInside {
return pointInside
}
for annotation in annotations(in: visibleMapRect) where annotation is <#CustomAnnotationViewClass#> {
guard let view = self.view(for: annotation as! MKAnnotation) else {
continue
}
if view.frame.contains(point) {
allowSelectionChanges = true
return true
}
}
allowSelectionChanges = false
return pointInside
}
}
I'm basing my work off of #clinton and #mihai-fratu. Both of them gave very good answers so you should up-vote them as well. What I want to add is that if the tapped annotation is in a cluster, or if it is disabled, then you will still get the deselection happening. This is my code to attempt to fix that.
public class <#CustomMapViewClass#>: MKMapView {
private var allowSelectionChanges: Bool = true
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return allowSelectionChanges
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let pointInside = super.point(inside: point, with: event)
if pointInside {
// Go through all annotations in the visible map rect
for annotation in annotations(in: visibleMapRect) where annotation is MKAnnotation {
// get the view of each annotation
if let view: MKAnnotationView = self.view(for: annotation as! MKAnnotation) {
// work with the cluster view if there is one
let rootView = view.cluster ?? view
// If the frame of this view contains the selected point, then we are an annotation tap. Allow the gesture...
if (rootView.frame.contains(point)) {
allowSelectionChanges = rootView.isEnabled // But only if the view is enabled
return pointInside
}
}
}
// If you did not tap in any valid annotation, disallow the gesture
allowSelectionChanges = false
}
return pointInside
}
}

Is there a general way to be notified of all touches began and moved at the UIViewController level?

I have a UIViewController subclass whose view has a complex hierarchy of descendant views, including UIButtons and a UISlider. I want to hide the controls after 5 seconds have passed without any user interaction. The 5 seconds part is easy: use a timer. I don't know of a general way to detect user interaction though.
My first thought was to override UIResponder's touchesBegan:withEvent: and touchesMoved:withEvent: in the UIViewController subclass, but those don't get called when the user taps a UIButton or drags the knob on the UISlider.
What I don't want to do is register for notifications from every control, if I can help it. I also don't want to mess with the UIWindow. Any other ideas?
I ended up creating a continuous gesture recognizer that recognizes when there is at least one touch. I added it to the UIViewController subclass's view and it works great! It took me a long time to realize that it needed both cancelsTouchesInView and delaysTouchesEnded set to false or else it would fail one way or another.
class TouchDownGestureRecognizer: UIGestureRecognizer {
override init(target: AnyObject, action: Selector) {
super.init(target: target, action: action)
cancelsTouchesInView = false
delaysTouchesEnded = false
}
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
state = .Began
}
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
if numberOfTouches() - touches.count == 0 {
state = .Ended
}
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
if numberOfTouches() - touches.count == 0 {
state = .Cancelled
}
}
override func shouldBeRequiredToFailByGestureRecognizer(otherGestureRecognizer: UIGestureRecognizer!) -> Bool {
return false
}
override func shouldRequireFailureOfGestureRecognizer(otherGestureRecognizer: UIGestureRecognizer!) -> Bool {
return false
}
override func canPreventGestureRecognizer(preventedGestureRecognizer: UIGestureRecognizer!) -> Bool {
return false
}
override func canBePreventedByGestureRecognizer(preventingGestureRecognizer: UIGestureRecognizer!) -> Bool {
return false
}
}
Add a UIGestureRecognizer to your UIViewController by calling setupTapGesture in viewDidLoad
- (void)setupTapGesture
{
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(toggleVisibility)];
tapGesture.delegate = self;
[self.view addGestureRecognizer:tapGesture];
}
And then use this callback to detect touches from button, sliders (UIControl)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//To ignore touches from Player Controls View
if ([[touch.view superview] isKindOfClass:[UIControl class]])
{
return NO;
}
return YES;
}
Why not use UIGestureRecognizer?. You might have to set up a few different ones for different possible interaction types like holds and swipes, but then you could have all of them call the same function that keeps the screen on and resets the timer.

How can I make touch events pass through a UIView (similar to pointer-events:none in CSS)?

In CSS, pointer-events:none; allows click events to pass through an element. I'm curious if there is anything similar that I can do in Objective-C on iOS for UIViews.
Here's a jsfiddle to an example of pointer-events: none.
Could I somehow achieve the same behavior for a UIView by overriding hitTest:withEvent:? Or maybe there is another way to do this?
Thanks for any help.
Here's a handy implementation in line with your initial intuition:
#import "TouchTransparentView.h"
#implementation TouchTransparentView
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
return nil;
} else {
return hitView;
}
}
#end
Swift version, with a #IB upgrade
import UIKit
#IBDesignable
class PassthroughTouchView: UIView {
#IBInspectable var passthroughTouchEvents : Bool = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self && passthroughTouchEvents == true {
return nil;
} else {
return hitView;
}
}
}
What you're looking for is commonly called "Event Bubbling".
You may refer to the Apple docs here:
https://developer.apple.com/documentation/uikit/understanding_event_handling_responders_and_the_responder_chain
So your basic instinct was right, you in fact do need utilize hitTest:withEvent:, but you can further customize things by hooking up into the Responder chain. That section has a very neat example.
In swift:
import UIKit
class PassThroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self {
return nil
} else {
return hitView
}
}
}
But having to do this kind of sucks...
If you are looking for something that acts like pointer-events:none I would use
yourView.userInteractionEnabled = NO
It should do basically the same thing.

Resources