Preventing annotation deselection in MKMapView - ios

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

Related

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.

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

Custom CalloutView with Swift 2.0 Best Approach?

I want to create a custom CalloutView from xib. I created a xib and custom AnnotationView class. I want to use my MapViewController segue which name is showDetail segue because this segue is a Push segue. When the user taps the button in my calloutView it should perform my showDetail segue.
I searched all documents, tutorials, guides and questions but I could
not find a any solution with Swift. There are no custom libraries also. I couldn't find any solution it as been 3 days.
My CalloutView.xib file View is my CustomAnnotationView which name is CalloutAnnotationView.swift and File's Owner is MapViewController it allows me to use MapViewController segues.
There is my CalloutAnnotationView.swift
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if (hitView != nil) {
self.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 self.subviews {
isInside = CGRectContainsPoint(view.frame, point)
if (isInside) {
}
}
}
return isInside
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let calloutViewController = MapViewController(nibName: "CalloutView", bundle: nil)
if selected {
calloutViewController.view.clipsToBounds = true
calloutViewController.view.layer.cornerRadius = 10
calloutViewController.view.center = CGPointMake(self.bounds.size.width * 0.5, -calloutViewController.view.bounds.size.height * 0.5)
self.addSubview(calloutViewController.view)
} else {
// Dismiss View
calloutViewController.view.removeFromSuperview()
}
}
My MapViewController.swift codes;
// MARK: - MapKit Delegate Methods
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? Veterinary { // Checking annotations is not User Location.
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(Constants.MapViewAnnotationViewReuseIdentifier) as? CalloutAnnotationView
if pinView == nil { // If it is nil it created new Pin View
pinView = CalloutAnnotationView(annotation: annotation, reuseIdentifier: Constants.MapViewAnnotationViewReuseIdentifier)
pinView?.canShowCallout = false
} else { pinView?.annotation = annotation } // If it is NOT nil it uses old annotations.
// Checking pin colors from Veterinary Class PinColor Method
pinView?.image = annotation.pinColors()
//pinView?.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
return pinView
}
return nil
}
These codes are working fine to load my xib file. I created IBAction from a button and It performs segues from MapViewControllerto my DetailViewController with showDetail segue. But problem is It doesn't remove my customCalloutView from superview so I can't dismiss callout views.
How can I solve this problem or What is the best way to create custom calloutView for use my MapViewController push segues ?
Thank you very much.
In your MKMapView delegate see if this gets called when segueing:
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if let annotation = view.annotation as? Veterinary {
mapView.removeAnnotation(annotation)
}
}
I hope this helps you with the problem
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if view.isKindOfClass(AnnotationView)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}

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 to prevent UINavigationBar from blocking touches on view

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

Resources